Using value inside MergeMany operator in another pipeline - swift

I created this function for decoding locally saved data.
private func getLocalCertificates(_ data: [CovidCertificateEntity]) -> [CovidCertificateDomainItem]? {
var newCertificates: [CovidCertificateDomainItem]?
Publishers
.MergeMany(
data.map { result -> AnyPublisher<Result<EUDCC, ErrorType>, Never> in
self.certificateToDelete = result.qrCodeValue ?? ""
return repository.getCertificateDetails(result.qrCodeValue ?? "")
})
.map { result -> CovidCertificateDomainItem? in
switch result {
case .success(let eudcc):
do {
return try CovidCertificateDomainItem(eudcc: eudcc, qrCode: self.certificateToDelete)
}
catch {
return nil
}
case.failure(_):
return nil
}
}
.compactMap { $0 }
.collect()
.sink { result in
newCertificates = result.reversed()
}
.store(in: &cancellables)
return newCertificates
}
I wanted to achieve that value result from data.map inside MergeMany operator is proceeded to .map operator, so I can use it in constructor of CovidCertificateDomainItem
I tried to made this with help variable certificateToDelete, but it always have last value from data.map.
Is there any way to achieve this?

Pass a tuple. Instead of returning
repository.getCertificateDetails(result.qrCodeValue ?? "")
return a tuple:
(result, repository.getCertificateDetails(result.qrCodeValue ?? ""))
You will have many other adjustments to make in order to achieve that, but when you do, you'll be able to receive that tuple in map and thus have all the data you need.

Related

Why can't I use .flatMap() after .tryMap() in Swift Combine?

I am studying and trying out a few stuff with Combine to apply on my own and came into the following situation with this contrived example..
let sequencePublisher = [70, 5, 17].publisher
var cancellables = [AnyCancellable]()
sequencePublisher
// .spellOut()
.flatMap { query -> URLSession.DataTaskPublisher in
return URLSession.shared.dataTaskPublisher(for: URL(string: "http://localhost:3000?q=\(query)")!)
}
.compactMap { String(data: $0.data, encoding: .utf8) }
.sink(receiveCompletion: { completion in
switch completion {
case .failure(let error):
print(error.localizedDescription)
default: print("finish")
}
}) { value in
print(value)
}
.store(in: &cancellables)
I have a sequence publisher that emits 3 Integers and I pass it through flatMap and send a Get request request to my local API that simply returns back the same value it got embedded in a string.
It all works fine, I get all 3 API responses in sink, as long as I don't uncomment the spellOut() custom operator, this operator is supposed to fail if the number is smaller than 6, here is what it does:
enum ConversionError: LocalizedError {
case lessThanSix(Int)
var errorDescription: String? {
switch self {
case .lessThanSix(let n):
return "could not convert number -> \(n)"
}
}
}
extension Publisher where Output == Int {
func spellOut() -> Publishers.TryMap<Self, String> {
tryMap { n -> String in
let formatter = NumberFormatter()
formatter.numberStyle = .spellOut
guard n > 6, let spelledOut = formatter.string(from: n as NSNumber) else { throw ConversionError.lessThanSix(n) }
return spelledOut
}
}
}
The code doesn't even compile if I add another map operator before flatMap it works, but with a tryMap it just says
No exact matches in call to instance method 'flatMap'
Is there any way of achieving this or why is it not allowed?
Thank you in advance for the answers
The problem here is that FlatMap requires the returned publisher created in its closure to have the same Failure type as its upstream (unless upstream has a Never failure).
So, a Sequence publisher, like:
let sequencePublisher = [70, 5, 17].publisher
has a failure type of Never and all works.
But TryMap, which is what .spellOut operator returns, has a failure type of Error, and so it fails, because DataTaskPublisher has a URLError failure type.
A way to fix is to match the error type inside the flatMap:
sequencePublisher
.spellOut()
.flatMap { query in
URLSession.shared.dataTaskPublisher(for: URL(...))
.mapError { $0 as Error }
}
// etc...
You have to map the error after the tryMap.
publisher
.tryMap({ id in
if let id = id { return id } else { throw MyError.unknown("noId") }
})
.mapError { $0 as? MyError ?? MyError.unknown("noId") }
.flatMap { id -> AnyPublisher<Model, MyError> in
fetchDataUseCase.execute(id: id)
}
.eraseToAnyPublisher()
In case the error types already match, another point of failure can be, when you work with any Publisher as return values. Then you need to call eraseToAnyPublisher() on the publishers - the first one and the one returned from the flatMap closure.
anyPublisher.eraseToAnyPublisher()
.flatMap { value in
anotherPublisher.eraseToAnyPublisher()
}

Value just available after second function call

I have the following function and it's working great but now I need the value out of the function to use it for another purpose.
But my problem is that I always have to execute the function twice to get a value in the outer part of the function (var valueOutOfFunction).
var valueOutOfFunction = [(String)]()
func loadQuery(com:#escaping( [(Int, String)] -> ())){
var arrayOfTuples = [(Int, String)]()
db.collection("Data").whereField("age", isGreaterThanOrEqualTo: 1).whereField("age", isLessThanOrEqualTo: 50).whereField("gender", isEqualTo: "F").getDocuments() { (querySnapshot, err) in
if let err = err {
print("Error getting documents: \(err)")
} else {
for (index, document) in querySnapshot!.documents.enumerated() {
arrayOfTuples += [(index, document.documentID)]
}
}
com(arrayOfTuples)
}
}
Then I am calling it here:
loadQuery { arr in
self.valueOutOfFunction = arr // Has a value in the first execution
}
print (valueOutOfFunction) //Executing the first time there is no value in the variable, the second time it have a value.
Why is it just on the second attempt available and what could be a solution for this problem ?
Thanks!
That is because the valueOutOfFunction is happening inside of that closure which is being called asynchronously to the print statement. Additionally, you are executing the completion outside of the else statement which may be firing before your for loop completes. What you want to do is control the flow from within the closure itself like so:
// move com(arrayOfTuples) up into the else block but outside of the for loop
func handleQuery() {
loadQuery { doStuffWithResult($0) }
// This line will run before the first line of doStuffWithResult
// It would be best to end the function logic at loadQuery
}
func doStuffWithResult(_ arrayOfTuples: [(Int, String)]) {
print(arrayOfTuples)
// Do other work here
}
What you're looking into is control flow. You want to execute X if Y has occurred and Z if !Y right?
I would recommend looking into swift's result type. This will help you manage control flow with closures.
An example:
typealias QueryResult = [(Int, String)]
enum QueryErrors: Error {
case couldNotFind
}
func loadQuery(_ result: #escaping (Result<QueryResult, Error>) -> Void) {
db.collection("Data").whereField("age", isGreaterThanOrEqualTo: 1).whereField("age", isLessThanOrEqualTo: 50).whereField("gender", isEqualTo: "F").getDocuments() { (querySnapshot, err) in
guard let documents = querySnapshot?.documents.enumerated() else {
// I think enumerated will give you an array of tuples... if not add this to the end of that line. After enumerated but before else
// enumerated().compactMap { ($0, $1) }
result(.failure(err ?? QueryErrors.couldNotFind))// passes back Firebase error if it exists or default error if it doesn't
return
}
result(.success(documents))
}
}
// how it works
loadQuery() { result in
switch(result) {
case .success(let arr): print(arr)
case .failure(let error): print(error)
}
}

Never return in a Flatmap with Swift's Combine

In RxSwift, a flatMap operator can easily return a non-completing Observable. Let's say we have this (contrived and silly) Observable chain:
let repo = DataRepository()
Observable
.just(Int.random(in: 0 ..< 1000))
.flatMap { num -> Observable<String> in
if num == 42 {
return .never()
}
return repo
.fetchData()
.filter { $0.statusCode == 200 }
.map { $0.data.title }
}
With Combine, the closest I can get is something like this (haven't tried to compile, but you get the idea):
Just(Int.random(in: 0 ..< 1000))
.flatMap { num -> AnyPublisher<String, Never> in
if num == 42 {
return Empty<String, Never>(completeImmediately: false).eraseToAnyPublisher()
}
return repo
.fetchData()
.filter { $0.statusCode == 200 }
.map { $0.data.title }
.eraseToAnyPublisher()
}
I'm okay-ish with this solution, but I see two problems that I would like to avoid:
1) The Combine solution is somewhat more verbose to achieve the same thing.
2) I have to call eraseToAnyPublisher() on both returned Publishers, else the return types don't match. I believe calling eraseToAnyPublisher() prevents Swift from applying some internal optimizations (I can't find the article I read about this optimization anymore; the information is scarce around this)
Does anyone have a better approach to handling this example scenario?
Try to lift any conditional logic into operators.
Conditions under which you emit something like Observable.never are best captured in a filter, that way you get the "never" behavior for free.
Example:
Just(Int.random(in: 0 ..< 1000))
.filter { $0 != 42 }
.flatMap {
return repo
.fetchData()
.filter { $0.statusCode == 200 }
.map { $0.data.title }
}
I don't know enough about the types in DataRepository to know if you need to type erase inside the flatMap closure.
The previous answer does not answer the original question, it only gives suggestions on how to avoid it.
But there are legitimate situations where this behavior is needed, such as a Timer, that you don't want to tick and consume resources.
The code in the question is almost correct, but it needs to be:
Just(Int.random(in: 0 ..< 1000))
.map { num -> AnyPublisher<String, Never> in
if num == 42 {
return Empty<String, Never>(completeImmediately: false).eraseToAnyPublisher()
}
return repo
.fetchData()
.filter { $0.statusCode == 200 }
.map { $0.data.title }
.eraseToAnyPublisher()
}
.switchToLatest()
A more practical use case, for example a timer that ticks every second, and can be paused.
let isPaused: AnyPublisher<Bool, Never>
let pausableTimer = isPaused.
.map { isPaused in
if isPaused {
return Empty<Void, Never>()
.eraseToAnyPublisher()
} else {
return Timer.publish(every: 1, on: .main, in: .default)
.autoconnect()
.map { _ in }
.eraseToAnyPublisher()
}
}
.switchToLatest()
You don't want to use someting like CombineLatest, because the Timer would keep on ticking, while it's paused and the output even is ignored.

How to handle Errors on never ending chain with materialize?

Imagine the following chain where a user wants to save a list of some sort:
var saveChain = userTappedSaveListSubject
.doOnNext { list -> Void in // create pdf version
let pdfFactory = ArticleListPDFFactory()
list.pdf = try pdfFactory.buildPDF(list)
try database.save(list)
}
.flatMap { list in
AuthorizedNetworking.shared.request(.createList(try ListRequestModel(list)))
.filter(statusCode: 201)
.map { _ in list }
}
.doOnNext { list in
list.uploaded = true
try database.save(list)
try Printer().print(list)
}
.materialize()
.share()
On every operator in the chain errors can occur, which would terminate the stream and the user would be unable to retry saving and printing the list (the whole chain gets disposed).
In the end the user should see either a "success" or "failure" screen by binding the observable to a textField:
Observable.of(
saveChain.elements().map { _ in
("List saved!", subtitle: "Saving successfull")
},
saveChain.errors().map { error in
("Error!", subtitle: error.localizedDescription)
})
.merge()
How should the error be handled?
Here's the obvious fix:
let saveChain = userTappedSaveListSubject
.flatMap { list in
Observable.just(list)
.do(onNext: { list -> Void in // create pdf version
let pdfFactory = ArticleListPDFFactory()
list.pdf = try pdfFactory.buildPDF(list)
try database.save(list)
})
.flatMap { list in
AuthorizedNetworking.shared.request(.createList(try ListRequestModel(list)))
.filter(statusCode: 201)
.map { _ in list }
}
.do(onNext: { list in
list.uploaded = true
try database.save(list)
try Printer().print(list)
})
.materialize()
}
.share()
However, there are a host of problems with this code because of the mixed paradigms.
You are passing around a mutable class inside your Observables. This is problematic because it's a functional paradigm so the system expects the contained type to be either a struct/enum or an immutable class.
Your reliance on side effects to load up said mutable class object again is quite odd and against the paradigm.

RxSwift map and flatMap difference

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.