RxSwift map and flatMap difference - swift

I can't understand the difference between map and flatMap In RxSwift. In the RxSwift playground examples and the books, flatMap is used as converting Observables which has inner Observable property.
However I see flatMap being used directly on Observable of basic types. For example for below code, both of them produces the same output. Can someone help me to understand the difference between map and flatMap
struct Student {
let score:Int
}
let ryan = Student(score:80)
let student = PublishSubject<Student>()
let deneme = student.map({ val in
return Student(score: val.score+10)
})
deneme.subscribe(onNext: {
print("StudentMAP: \($0.score)")
})
let deneme2 = student.flatMap({ val -> Observable<Student> in
return Observable.of(Student(score: val.score + 10))
})
deneme2.subscribe(onNext: {
print("StudentFlatMAP: \($0.score)")
})
student.onNext(ryan)

map get value from stream and return another value of whatever type, result is Observable< whatever type >.
flatMap get value from stream and return an Observable of whatever type.
This means you can use flatMap when:
you already have a function declared which returns Observable< ? >, so you may want to use it in flatMap
func foo(_ number: Int) -> Observable<String> {
return Observable.just(String(number))
}
Observable.just(1)
.flatMap { (number) -> Observable<String> in
return foo(number)
}
you need that returned value push more than one value in the stream
func updates() -> Observable<String> {
// Something that generates updates
}
func shouldListenToUpdated() -> Observable<Bool> {
return Observable.just(true)
}
shouldListenToUpdated()
.flatMap { (listenToUpdated) -> Observable<String> in
return listenToUpdated ? updates() : Observable.empty()
}
While map will just transform next value in the stream.
Hope this clarifies things a bit.

To keep it simple
Use flatMap when you want return Observable down the stream.
Use map is simply transform the value of the observable and pass down the stream
Flatmap:
response.flatMap { response, _ -> Observable<NSString> in
guard let value = response.allHeaderFields["string"] as? NSString
else {
return Observable.empty()
}
return Observable.just(value)
}.subscribe(onNext: { [weak self] string in
print(string)
}).disposed(by: bag)
Map:
response.filter { response, _ in
return 200..<300 ~= response.statusCode
}.map { _ , data -> [[String: Any]] in
guard let jsonObject = try? JSONSerialization.jsonObject(with: data, options: []),
let result = jsonObject as? [[String: Any]] else {
return []
}
return result
}.subscribe(onNext: { [weak self] objects in
print(objects)
}).disposed(by: bag)

flatMap is similar to map, but it transforms element of observable to an observable of sequences. The example you use is relatively simple, it is simply sending and Observable mapped into something else.
Here is quote from Reactive extension documentation,
The FlatMap operator transforms an Observable by applying a function
that you specify to each item emitted by the source Observable, where
that function returns an Observable that itself emits items. FlatMap
then merges the emissions of these resulting Observables, emitting
these merged results as its own sequence.
This method is useful, for example, when you have an Observable that
emits a series of items that themselves have Observable members or are
in other ways transformable into Observables, so that you can create a
new Observable that emits the complete collection of items emitted by
the sub-Observables of these items.
If you extend the example a bit, you will know that flatMap actually transforms each element into a sequence.
Notice that you used,
student.onNext(ryan)
Remove your dename2 and add this code below,
let studentObservable: PublishSubject<Student> = PublishSubject()
let deneme2 = student.flatMap({ val -> Observable<Student> in
return studentObservable.map { val in Student(score: val.score + 10) }
})
deneme2.subscribe(onNext: {
print("StudentFlatMAP: \($0.score)")
})
student.onNext(ryan)
studentObservable.onNext(Student(score: 80))
studentObservable.onNext(Student(score: 90))
studentObservable.onNext(Student(score: 100))
Now, you can see that map would simply transform a value from sequence and new Observable is created, while flatMap transforms it into sequence. Now, each of the flatMapped elements can themselves emit values since they are stream themselves.

Related

Chaining n requests in Combine [duplicate]

This question already has answers here:
Combine framework serialize async operations
(8 answers)
Closed 1 year ago.
I am trying to chain n requests with Combine.
Let's assume I have 50 users and for each of them I need to do a single request to get a users data. I know that with flatMap you can pass one Publisher result into the next. But does that work with loops as well?
That's my function to fetch a user:
func fetchUser(for id: Int) -> AnyPublisher<User, Error> {
let url = "https://user.com/api/user/\(id)"
return URLSession.shared.dataTaskPublisher(for: url)
.mapError { $0 as Error }
.map { $0.data }
.decode(type: User.self, decoder: JSONDecoder())
.eraseToAnyPublisher()
}
So basically I need another function, which loops this over this fetchUser and returns all users in one result array. The requests should not all run at the same time, but rather start one after the previous one has finished.
For this one, a lot depends on how you want to use the User objects. If you want them to all emit as individual users as they come in, then merge is the solution. If you want to keep the array order and emit them all as an array of users once they all come in, then combineLatest is what you need.
Since you are dealing with an array, and neither merge nor combineLatest have array versions, you will need to use reduce. Here's examples:
func combine(ids: [Int]) -> AnyPublisher<[User], Error> {
ids.reduce(Optional<AnyPublisher<[User], Error>>.none) { state, id in
guard let state = state else { return fetchUser(for: id).map { [$0] }.eraseToAnyPublisher() }
return state.combineLatest(fetchUser(for: id))
.map { $0.0 + [$0.1] }
.eraseToAnyPublisher()
}
?? Just([]).setFailureType(to: Error.self).eraseToAnyPublisher()
}
func merge(ids: [Int]) -> AnyPublisher<User, Error> {
ids.reduce(Optional<AnyPublisher<User, Error>>.none) { state, id in
guard let state = state else { return fetchUser(for: id).eraseToAnyPublisher() }
return state.merge(with: fetchUser(for: id))
.eraseToAnyPublisher()
}
?? Empty().eraseToAnyPublisher()
}
Notice that in the combine case, if the array is empty, the Publisher will emit an empty array then complete. In the merge case, it will just complete without emitting anything.
Also notice that in either case if any of the Publishers fail, then the entire chain will shut down. If you don't want that, you will have to catch the errors and do something with them...

Generic parameter 'Result' could not be inferred with RxSwift

I am new to Swift, so maybe the question is a little bit stupid. I don't know why I got the error here:
htmlHelper.fetchHtmlObservable(url) // this one is an Observable<String> function
.subscribeOn(ConcurrentDispatchQueueScheduler.init(qos: .background))
.map { //Generic parameter 'Result' could not be inferred
let parsingHelper = ParsingTypeFactory.getParsingType(parsingType: self.parsingType)
return parsingHelper.parseActionItems(document: $0)
}
To resolve it, I had to add
.map { doc -> [MyItem] in
let parsingHelper = ParsingTypeFactory.getParsingType(parsingType: self.parsingType)
return parsingHelper.parseActionItems(document: doc)
}
When can I omit the params and the return type?
In order for type inference to work in closures, you generally need either the outer scope to know the type or the closure itself needs to be one line. This is a limitation in the Swift type system. So either:
htmlHelper.fetchHtmlObservable(url) // this one is an Observable<String> function
.subscribeOn(ConcurrentDispatchQueueScheduler.init(qos: .background))
.map {
ParsingTypeFactory.getParsingType(parsingType: self.parsingType).parseActionItems(document: $0)
}
or
let myItems: Observable<[MyItem]> = htmlHelper.fetchHtmlObservable(url) // this one is an Observable<String> function
.subscribeOn(ConcurrentDispatchQueueScheduler.init(qos: .background))
.map {
let parsingHelper = ParsingTypeFactory.getParsingType(parsingType: self.parsingType)
return parsingHelper.parseActionItems(document: $0)
}
Other options:
Note that in all of the cases discussed so far, you are holding a strong reference to self and likely causing a memory cycle/leak. You can avoid that by making a helper function that isn't part of the class:
// do NOT put this in the class, make it a free function (possibly private to avoid namespace pollution.)
func parser(for parsingType: ParsingType) -> (Document) -> [MyItem] {
return { document in
let parsingHelper = ParsingTypeFactory.getParsingType(parsingType: parsingType)
return parsingHelper.parseActionItems(document: document)
}
}
And now the code in question becomes:
let myItems = htmlHelper.fetchHtmlObservable(url) // this one is an Observable<String> function
.subscribeOn(ConcurrentDispatchQueueScheduler.init(qos: .background))
.map(parser(for: parsingType))
If you don't like the idea of a free function, or you don't like a function that returns a function, you can put the function in an extension on ParserType:
extension ParsingType {
func parser(document: Document) -> [MyItem] {
let parsingHelper = ParsingTypeFactory.getParsingType(parsingType: self)
return parsingHelper.parseActionItems(document: document)
}
}
and now the original code becomes:
let myItems = htmlHelper.fetchHtmlObservable(url) // this one is an Observable<String> function
.subscribeOn(ConcurrentDispatchQueueScheduler.init(qos: .background))
.map(parsingType.parser(document:))
This also avoids keeping a reference to self.

Swift Combine: How to create a single publisher from a list of publishers?

Using Apple's new Combine framework I want to make multiple requests from each element in a list. Then I want a single result from a reduction of all the the responses. Basically I want to go from list of publishers to a single publisher that holds a list of responses.
I've tried making a list of publishers, but I don't know how to reduce that list into a single publisher. And I've tried making a publisher containing a list but I can't flat map a list of publishers.
Please look at the "createIngredients" function
func createIngredient(ingredient: Ingredient) -> AnyPublisher<CreateIngredientMutation.Data, Error> {
return apollo.performPub(mutation: CreateIngredientMutation(name: ingredient.name, optionalProduct: ingredient.productId, quantity: ingredient.quantity, unit: ingredient.unit))
.eraseToAnyPublisher()
}
func createIngredients(ingredients: [Ingredient]) -> AnyPublisher<[CreateIngredientMutation.Data], Error> {
// first attempt
let results = ingredients
.map(createIngredient)
// results = [AnyPublisher<CreateIngredientMutation.Data, Error>]
// second attempt
return Publishers.Just(ingredients)
.eraseToAnyPublisher()
.flatMap { (list: [Ingredient]) -> Publisher<[CreateIngredientMutation.Data], Error> in
return list.map(createIngredient) // [AnyPublisher<CreateIngredientMutation.Data, Error>]
}
}
I'm not sure how to take an array of publishers and convert that to a publisher containing an array.
Result value of type '[AnyPublisher]' does not conform to closure result type 'Publisher'
Essentially, in your specific situation you're looking at something like this:
func createIngredients(ingredients: [Ingredient]) -> AnyPublisher<[CreateIngredientMutation.Data], Error> {
Publishers.MergeMany(ingredients.map(createIngredient(ingredient:)))
.collect()
.eraseToAnyPublisher()
}
This 'collects' all the elements produced by the upstream publishers and – once they have all completed – produces an array with all the results and finally completes itself.
Bear in mind, if one of the upstream publishers fails – or produces more than one result – the number of elements may not match the number of subscribers, so you may need additional operators to mitigate this depending on your situation.
The more generic answer, with a way you can test it using the EntwineTest framework:
import XCTest
import Combine
import EntwineTest
final class MyTests: XCTestCase {
func testCreateArrayFromArrayOfPublishers() {
typealias SimplePublisher = Just<Int>
// we'll create our 'list of publishers' here. Each publisher emits a single
// Int and then completes successfully – using the `Just` publisher.
let publishers: [SimplePublisher] = [
SimplePublisher(1),
SimplePublisher(2),
SimplePublisher(3),
]
// we'll turn our array of publishers into a single merged publisher
let publisherOfPublishers = Publishers.MergeMany(publishers)
// Then we `collect` all the individual publisher elements results into
// a single array
let finalPublisher = publisherOfPublishers.collect()
// Let's test what we expect to happen, will happen.
// We'll create a scheduler to run our test on
let testScheduler = TestScheduler()
// Then we'll start a test. Our test will subscribe to our publisher
// at a virtual time of 200, and cancel the subscription at 900
let testableSubscriber = testScheduler.start { finalPublisher }
// we're expecting that, immediately upon subscription, our results will
// arrive. This is because we're using `just` type publishers which
// dispatch their contents as soon as they're subscribed to
XCTAssertEqual(testableSubscriber.recordedOutput, [
(200, .subscription), // we're expecting to subscribe at 200
(200, .input([1, 2, 3])), // then receive an array of results immediately
(200, .completion(.finished)), // the `collect` operator finishes immediately after completion
])
}
}
I think that Publishers.MergeMany could be of help here. In your example, you might use it like so:
func createIngredients(ingredients: [Ingredient]) -> AnyPublisher<CreateIngredientMutation.Data, Error> {
let publishers = ingredients.map(createIngredient(ingredient:))
return Publishers.MergeMany(publishers).eraseToAnyPublisher()
}
That will give you a publisher that sends you single values of the Output.
However, if you specifically want the Output in an array all at once at the end of all your publishers completing, you can use collect() with MergeMany:
func createIngredients(ingredients: [Ingredient]) -> AnyPublisher<[CreateIngredientMutation.Data], Error> {
let publishers = ingredients.map(createIngredient(ingredient:))
return Publishers.MergeMany(publishers).collect().eraseToAnyPublisher()
}
And either of the above examples you could simplify into a single line if you prefer, ie:
func createIngredients(ingredients: [Ingredient]) -> AnyPublisher<CreateIngredientMutation.Data, Error> {
Publishers.MergeMany(ingredients.map(createIngredient(ingredient:))).eraseToAnyPublisher()
}
You could also define your own custom merge() extension method on Sequence and use that to simplify the code slightly:
extension Sequence where Element: Publisher {
func merge() -> Publishers.MergeMany<Element> {
Publishers.MergeMany(self)
}
}
func createIngredients(ingredients: [Ingredient]) -> AnyPublisher<CreateIngredientMutation.Data, Error> {
ingredients.map(createIngredient).merge().eraseToAnyPublisher()
}
To add on the answer by Tricky, here is a solution which retains the order of elements in the array.
It passes an index for each element through the whole chain, and sorts the collected array by the index.
Complexity should be O(n log n) because of the sorting.
import Combine
extension Publishers {
private struct EnumeratedElement<T> {
let index: Int
let element: T
init(index: Int, element: T) {
self.index = index
self.element = element
}
init(_ enumeratedSequence: EnumeratedSequence<[T]>.Iterator.Element) {
index = enumeratedSequence.offset
element = enumeratedSequence.element
}
}
static func mergeMappedRetainingOrder<InputType, OutputType>(
_ inputArray: [InputType],
mapTransform: (InputType) -> AnyPublisher<OutputType, Error>
) -> AnyPublisher<[OutputType], Error> {
let enumeratedInputArray = inputArray.enumerated().map(EnumeratedElement.init)
let enumeratedMapTransform: (EnumeratedElement<InputType>) -> AnyPublisher<EnumeratedElement<OutputType>, Error> = { enumeratedInput in
mapTransform(enumeratedInput.element)
.map { EnumeratedElement(index: enumeratedInput.index, element: $0)}
.eraseToAnyPublisher()
}
let sortEnumeratedOutputArrayByIndex: ([EnumeratedElement<OutputType>]) -> [EnumeratedElement<OutputType>] = { enumeratedOutputArray in
enumeratedOutputArray.sorted { $0.index < $1.index }
}
let transformToNonEnumeratedArray: ([EnumeratedElement<OutputType>]) -> [OutputType] = {
$0.map { $0.element }
}
return Publishers.MergeMany(enumeratedInputArray.map(enumeratedMapTransform))
.collect()
.map(sortEnumeratedOutputArrayByIndex)
.map(transformToNonEnumeratedArray)
.eraseToAnyPublisher()
}
}
Unit test for the solution:
import XCTest
import Combine
final class PublishersExtensionsTests: XCTestCase {
// MARK: - Private properties
private var cancellables = Set<AnyCancellable>()
// MARK: - Tests
func test_mergeMappedRetainingOrder() {
let expectation = expectation(description: "mergeMappedRetainingOrder publisher")
let numbers = (1...100).map { _ in Int.random(in: 1...3) }
let mapTransform: (Int) -> AnyPublisher<Int, Error> = {
let delayTimeInterval = RunLoop.SchedulerTimeType.Stride(Double($0))
return Just($0)
.delay(for: delayTimeInterval, scheduler: RunLoop.main)
.setFailureType(to: Error.self)
.eraseToAnyPublisher()
}
let resultNumbersPublisher = Publishers.mergeMappedRetainingOrder(numbers, mapTransform: mapTransform)
resultNumbersPublisher.sink(receiveCompletion: { _ in }, receiveValue: { resultNumbers in
XCTAssertTrue(numbers == resultNumbers)
expectation.fulfill()
}).store(in: &cancellables)
waitForExpectations(timeout: 5)
}
}
You can do it in one line:
.flatMap(Publishers.Sequence.init(sequence:))

Unable to infer complex closure return type; add explicit type to disambiguate in RxSwift

I need to make multiple calls.
1. Delete Document Upload
2. Image 1 & server returns URL
3. Upload Image 2 & server returns URL
4. Create Document API contains both URLs & extra
parameters.
The code which I tried to write is in RxSwift,& MVVM.
let resultOfDocumentUpdateWithDelete =
donepressed
.filter{ $0 }
.withLatestFrom(self.existingDocumentIDChangedProperty)
.flatMapLatest {id in
let deleted_document = apiClient.deleteDocument(id).asObservable().materialize()
let upload_frontImage = deleted_document
.withLatestFrom(self.frontImageNameChangedProperty)
.flatMapLatest {image in
apiClient.uploadImage(image: image!).asObservable().materialize()
}
let upload_backImage = upload_frontImage
.withLatestFrom(self.backImageChangedProperty)
.flatMapLatest {image in
apiClient.uploadImage(image: image!).asObservable().materialize()
}
let upload_document = upload_backImage
.withLatestFrom(self.parametersChangedProperty)
.flatMapLatest {parameters in
apiClient.uploadDocument(parameters: parameters)
}
return upload_document.materialize()
}
.share(replay: 1)
Make sure, two responses of server are input in last API, so all of these will be called in a sequence.
how to do in RxSwift.
This was an interesting one! The take-away here is that when you are in doubt, go ahead and make your own operator. If it turns out that you later figure out how to do the job using the built-in operators, then you can replace yours. The only thing with making your own is that they require a lot more testing.
Note, to use the below, you will have to combineLatest of your observables and then flatMap and pass their values into this function.
// all possible results from this job.
enum ProcessResult {
case success
case deleteFailure(Error)
case imageFailue(Error)
case backImageFailure(Error)
case documentFailure(Error)
}
func uploadContent(apiClient: APIClient, documentID: Int, frontImage: UIImage, backImage: UIImage, parameters: Parameters) -> Single<ProcessResult> {
// instead of trying to deal with all the materializes, I decided to turn it into a single process.
return Single.create { observer in
// each api call happens in turn. Note that there are no roll-back semantics included! You are dealing with a very poorly written server.
let deleted = apiClient.deleteDocument(id: documentID)
.asObservable()
.share()
let imagesUploaded = deleted
.flatMap { _ in Observable.zip(apiClient.uploadImage(image: frontImage).asObservable(), apiClient.uploadImage(image: backImage).asObservable()) }
.share()
let documentUploaded = imagesUploaded
.flatMap { arg -> Single<Void> in
let (frontURL, backURL) = arg
var updatedParams = parameters
// add frontURL and backURL to parameters
return apiClient.uploadDocument(parameters: updatedParams)
}
.share()
let disposable = deleted
.subscribe(onError: { observer(.success(ProcessResult.deleteFailure($0))) })
let disposable1 = imagesUploaded
.subscribe(onError: { observer(.success(ProcessResult.imageFailue($0))) })
let disposable2 = documentUploaded
.subscribe(
onNext: { observer(.success(ProcessResult.success)) },
onError: { observer(.success(ProcessResult.documentFailure($0))) }
)
return Disposables.create([disposable, disposable1, disposable2])
}
}

Binding an RxSwift Observable to an array

Trying out RxSwift and trying to convert my network calls. I can't seem to display my data in the view because im not sure how I convert my observable to something my views can use. Here's an example of my request:
class SomeService {
let provider = Provider()
func getData() -> Observable<[Object]?> { // Returns json
return provider
.request(.getSomething())
.debug()
.mapArrayOptional(type: Object.self)
// Using Moya_Modelmapper map each item in the array
}
}
In my view controller I get the data:
let data = Service.getData()
print(data) ... <Swift.Optional<Swift.Array<MyApp.Item>>>
I have tried to subscribe to the response to the sequence but I don't know how I actually convert it to something like an array I can use in my view.
UPDATE: With answer implemented:
func itemsObserver() {
print("Time to print step 1") // This gets printed
data
.filter { $0 != nil }.map { $0! }
.subscribe(
onNext: { objects in
print(objects as Any)
print("Step 2") // This does not get executed at all
},
onCompleted:{ objects in
print(objects as Any) // This is ()
print("Complete") // This gets printed
}
).addDisposableTo(disposeBag)
}
itemsObserver()
Console output:
Time to print step 1
Service.swift:21 (getData()) -> subscribed
Service.swift:21 (getData()) -> Event next(Status Code: 200, Data Length: 141)
Service.swift:21 (getData()) -> Event completed
Service.swift:21 (getData()) -> isDisposed
()
Complete
Update:
If your onNext block isn't getting called at all, it's because data never produced anything. Either your producer isn't producing any objects or mapArrayOptional is not transforming them.
The onCompleted block doesn't accept any arguments so the objects var you have in it is meaningless/Void.
Try this:
let data = service.getData()
data
.filter { $0 != nil }.map { $0! } // this removes the optionality of the result.
.subscribe(onNext: { objects in
// in here `objects` will be an array of the objects that came through.
}).disposed(by: bag)