RxSwift multiple observable in map - rx-swift

I ran into a situation where I would fetch an API which will generate json data of registered users. I would then have to loop through each user and fetch their avatar from remote url and save it to disk. I can perform this second task inside subscribe but this is not a best practice. I am trying to implement it with map, flatMap etc.
Here is my sample code:
self.dataManager.getUsers()
.observeOn(MainScheduler.instance)
.subscribeOn(globalScheduler)
.map{ [unowned self] (data) -> Users in
var users = data
// other code for manipulating users goes here
// then below I am trying to use another loop to fetch their avatars
if let cats = users.categories {
for cat in cats {
if let profiles = cat.profiles {
for profile in profiles {
if let thumbnail = profile.thumbnail,
let url = URL(string: thumbnail) {
URLSession.shared.rx.response(request: URLRequest(url: url))
.subscribeOn(MainScheduler.instance)
.subscribe(onNext: { response in
// Update Image
if let img = UIImage(data: response.data) {
try? Disk.save(img, to: .caches, as: url.lastPathComponent)
}
}, onError: { (error) in
}).disposed(by: self.disposeBag)
}
}
}
}
}
return users
}
.subscribe(onSuccess: { [weak self] (users) in
}).disposed(by: disposeBag)
There are 2 problems in this code. First is with the rx on URLSession which execute the task in background on another thread and there is no way to acknowledge the main subscribe back when this operation will finish. Second is with the loop and rx which is not efficient as it should generate multiple observables and then process it.
Any idea to improve this logic is welcome.

This was a fun puzzle.
The "special sauce" that solves the problem is in this line:
.flatMap {
Observable.combineLatest($0.map {
Observable.combineLatest(
Observable.just($0.0),
URLSession.shared.rx.data(request: $0.1)
.materialize()
)
})
}
The map before the line creates an Observable<[(URL, URLRequest)]> and the line in question converts it to an Observable<[(URL, Event<Data>)]>.
The line does this by:
Set up the network call to create an Observable<Data>
Materialize it to create an Observable<Event<Data>> (this is done so an error in one download won't shutdown the entire stream.)
Lift the URL back into an Observable which gives us an Observable<URL>
Combine the observables from steps 2 & 3 to produce an Observable<(URL, Event<Data>)>.
Map each array element to produce [Observable<(URL, Event<Data>)>]
Combine the observables in that array to finally produce Observable<[(URL, Event<Data>)]>
Here is the code
// manipulatedUsers is for the code you commented out.
// users: Observable<Users>
let users = self.dataManager.getUsers()
.map(manipulatedUsers) // manipulatedUsers(_ users: Users) -> Users
.asObservable()
.share(replay: 1)
// this chain is for handling the users object. You left it blank in your code so I did too.
users
.observeOn(MainScheduler.instance)
.subscribe(onNext: { users in
})
.disposed(by: disposeBag)
// This navigates through the users structure and downloads the images.
// images: Observable<(URL, Event<Data>)>
let images = users.map { $0.categories ?? [] }
.map { $0.flatMap { $0.profiles ?? [] } }
.map { $0.compactMap { $0.thumbnail } }
.map { $0.compactMap { URL(string: $0) } }
.map { $0.map { ($0, URLRequest(url: $0)) } }
.flatMap {
Observable.combineLatest($0.map {
Observable.combineLatest(
Observable.just($0.0),
URLSession.shared.rx.data(request: $0.1)
.materialize()
)
})
}
.flatMap { Observable.from($0) }
.share(replay: 1)
// this chain filters out the errors and saves the successful downloads.
images
.filter { $0.1.element != nil }
.map { ($0.0, $0.1.element!) }
.map { ($0.0, UIImage(data: $0.1)!) }
.observeOn(MainScheduler.instance)
.bind(onNext: { url, image in
try? Disk.save(image, to: .caches, as: url.lastPathComponent)
return // need two lines here because this needs to return Void, not Void?
})
.disposed(by: disposeBag)
// this chain handles the download errors if you want to.
images
.filter { $0.1.error != nil }
.bind(onNext: { url, error in
print("failed to download \(url) because of \(error)")
})
.disposed(by: disposeBag)

Related

SwiftUI: How to drag and drop an email from Mail on macOS

As a follow up on #Asperi's answer of my question on how to drag and drop contacts and, I'd also like to be able to drag and drop email in the same way. Here is my code:
import SwiftUI
import UniformTypeIdentifiers
let uttypes = [String(kUTTypeEmailMessage)]
struct ContentView: View
{
let dropDelegate = EmailDropDelegate()
var body: some View
{
VStack
{
Text("Drag your email here!")
.padding(20)
}
.onDrop(of: uttypes, delegate: dropDelegate)
}
}
struct EmailDropDelegate: DropDelegate
{
func validateDrop(info: DropInfo) -> Bool
{
return true
}
func dropEntered(info: DropInfo)
{
print ("Drop Entered")
}
func performDrop(info: DropInfo) -> Bool
{
let items = info.itemProviders(for: uttypes)
for item in items
{
print (item.registeredTypeIdentifiers) // prints []
item.loadDataRepresentation(forTypeIdentifier: kUTTypeEmailMessage as String, completionHandler: { (data, error) in
if let data = data
{
print(data)
}
})
}
return true
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
I'm not getting any data back that I can decode.
2020-11-08 09:34:54.877532+0000 DropContact[3856:124769] Cannot find representation conforming to type public.email-message
This feature has been eluding me forever so any help would be very much appreciated.
Well... the approach is the same, the only thing is that Apple Mail does not provide kUTTypeEmailMessage UTI representation on drag (copy)
If we register self for generic kUTTypeContent UTI and investigate content of pasteboard on drop mail from Mail, we get:
Ie, here is a complete list of representations:
com.apple.mail.PasteboardTypeMessageTransfer,
com.apple.mail.PasteboardTypeAutomator,
com.apple.pasteboard.promised-file-url,
dyn.ah62d4rv4gu8y6y4usm1044pxqzb085xyqz1hk64uqm10c6xenv61a3k,
NSPromiseContentsPboardType,
com.apple.pasteboard.promised-file-content-type,
dyn.ah62d4rv4gu8yc6durvwwa3xmrvw1gkdusm1044pxqyuha2pxsvw0e55bsmwca7d3sbwu,
Apple files promise pasteboard type,
public.url,
CorePasteboardFlavorType 0x75726C20,
dyn.ah62d4rv4gu8yc6durvwwaznwmuuha2pxsvw0e55bsmwca7d3sbwu,
Apple URL pasteboard type,
public.url-name,
CorePasteboardFlavorType 0x75726C6E,
public.utf8-plain-text,
NSStringPboardType
so now you can load data of any of those types from above (except of course Apple's own privates). And, by the way, that list might (and rather will) depend on macOS version.
I have no solution for this but might be on a path to it.
As I mentioned in a comment, it looks to me that SwiftUI does not have a way to fulfil file promises yet.
Afaik Mail, Photos and Safari using file promises while dragging images or mails.
This might help some one else with a solution.
func performDrop(info: DropInfo) -> Bool {
let pasteboard = NSPasteboard(name: .drag)
guard let items = pasteboard.pasteboardItems else { return false }
// it returns the content of the public.url → is an encoded message url
print(pasteboard.readObjects(forClasses: [NSURL.self]))
// getting available types for pasteboard
let types = NSFilePromiseReceiver.readableDraggedTypes.map { NSPasteboard.PasteboardType($0) }
for type in types {
for item in items {
print("type:", type)
print(item.data(forType: type))
}
}
}
This prints this:
Optional([message:%3Cf6304df5.BAAAA6Ge1-UAAAAAAAAAALTyTrMAAVNI2qYAAAAAAAcXzQBjqtkK#mailjet.com%3E])
type: NSPasteboardType(_rawValue: com.apple.NSFilePromiseItemMetaData)
nil
type: NSPasteboardType(_rawValue: dyn.ah62d4rv4gu8yc6durvwwa3xmrvw1gkdusm1044pxqyuha2pxsvw0e55bsmwca7d3sbwu)
nil
type: NSPasteboardType(_rawValue: com.apple.pasteboard.promised-file-content-type)
Optional(20 bytes)
But I don't know what data type com.apple.pasteboard.promised-file-content-type is … Maybe some kind of NSFilePromiseReceiver.
Maybe this helps someone.
Edit
Tried something else, maybe something more promising.
func performDrop(info: DropInfo) -> Bool {
let pasteboard = NSPasteboard(name: .drag)
guard let filePromises = pasteboard.readObjects(forClasses: [NSFilePromiseReceiver.self], options: nil) else { return false }
guard let receiver = filePromises.first as? NSFilePromiseReceiver else { return false }
let queue = OperationQueue.main
receiver.receivePromisedFiles(atDestination: URL.temporaryDirectory, operationQueue: queue) { (url, error) in
print(url, error)
}
}
In this example I'm able to get the NSFilePromiseReceiver but something is off.
After around 20 to 30s the callback is called and I get the URL finally – so there is maybe some potential to improve. I think it has something todo with the Queue.
Edit
I was able to move/copy the dropped mail into the download folder.
func performDrop(info: DropInfo) -> Bool {
let pasteboard = NSPasteboard(name: .drag)
guard let filePromises = pasteboard.readObjects(forClasses: [NSFilePromiseReceiver.self], options: nil) else { return false }
guard let receiver = filePromises.first as? NSFilePromiseReceiver else { return false }
let dispatchGroup = DispatchGroup()
let queue = OperationQueue()
let destUrl = URL.downloadsDirectory
dispatchGroup.enter()
var urls: [URL] = []
receiver.receivePromisedFiles(atDestination: destUrl, operationQueue: queue) { (url, error) in
if let error = error {
print(error)
} else {
urls.append(url)
}
print(receiver.fileNames, receiver.fileTypes)
dispatchGroup.leave()
}
dispatchGroup.notify(queue: .main, execute: {
print(urls)
})
}

RxSwift form validation and sending request in one stream

I have a case where I would like to validate form and then if everything is ok go to api request.
I've written some code and it works fine but errors dispose my stream. I know I could add .catch error at the end of flat map but then next flat map would be executed.
Can I add catch error at the end of stream without disposing it? Or the only way to deal with it is separate it to two streams validation and server responses?
enum Response {
case error(message: String)
case success
}
let start = input.validate
.withLatestFrom(input.textFields)
.flatMap { [unowned self] fields -> Observable<String> in
return self.validate(characters: fields)
}
.flatMapLatest { [unowned self] code -> Observable<String> in
return self.apiClient.rxSendData(code)
.retry(1)
}
.map { _ in return Response.success }
.asDriver { Driver.just(Response.error(message: $0.localizedDescription)) }
I'm making some assumptions about code you aren't showing. Your validate function is especially odd to me. It looks like it emits a String (which is ignored, if validation was successful and doesn't emit anything (or maybe an error) if validation failed?
let start = input.validate
.withLatestFrom(input.textFields)
.flatMapLatest { [unowned self] fields -> Observable<String> in
return self.validate(characters: fields)
.catchError { _ in Observable.empty() } // empty() doesn't emit a value so the next flatMap won't be executed.
}
.flatMapLatest { [unowned self] _ -> Observable<Response> in
return self.apiClient.rxSendData()
.retry(1)
.map { _ in Response.success }
.catchError { error in Observable.just(Response.error(message: error.localizedDescription)) }
}
.asDriver { Driver.just(Response.error(message: $0.localizedDescription)) }
If validate emits an error when validation fails, and you want to capture that error, then something like this would work:
let start = input.validate
.withLatestFrom(input.textFields)
.flatMapLatest { [unowned self] fields -> Observable<Response> in
return self.validate(characters: fields)
.map { _ in Response.success }
.catchError { Observable.just(Response.error(message: $0.localizedDescription)) }
}
.flatMapLatest { [unowned self] validation -> Observable<Response> in
// here, the above flatMap emits a value no matter what, so we have to switch on it to determine if we want to continue or just push the Response down the pipe.
switch validation {
case .error:
return Observable.just(validation)
case .success:
return self.apiClient.rxSendData()
.retry(1)
.map { _ in Response.success }
.catchError { error in Observable.just(Response.error(message: error.localizedDescription)) }
}
}
.asDriver { Driver.just(Response.error(message: $0.localizedDescription)) }
Have you considered the materialize operator? It converts an observable sequence into an observable sequence of event objects detailing what happened that cannot error but completes when the input sequence completes. You can then share that.
Something like:
let code = input.validate
.withLatestFrom(input.textFields)
.flatMap { [unowned self] fields -> Observable<String> in
self.validate(characters: fields)
.materialize()
}
.share(replay: 1)
code
.compactMap { $0.error }
.subscribe() // Show error from `self.validate`
.disposed(by: bag)
let request = code
.compactMap { $0.element }
// Will get to this flat map only if `self.validate` did not error
.flatMapLatest { [unowned self] code -> Observable<String> in
self.apiClient.rxSendData(code)
.retry(1)
.materialize()
}
.share(replay: 1)
request
.compactMap { $0.error }
.subscribe() // Show error from `self.apiClient.rxSendData`
.disposed(by: bag)
request
.compactMap { $0.element }
// Do something as a result of the request being successful
The chains would not cease upon self.validate and self.apiClient.rxSendData emitting errors.

RxSwift: Calling onCompleted after onNext delivers only the completed event

I'm wrapping some legacy completion-block code in an Observable. It will emit one event (next or error), and then complete. The problem is that calling onNext(), onCompleted() only sends the completed event to the observer. Why doesn't the next event get delivered?
UPDATE: The people stream actually works as expected. The issue turns out to be in the next stream, filteredPeople. The inner completed event is passed along to it, and I'm just returning it, which terminates the stream.
I need to filter out completed events from inner streams.
let people = Observable<Event<[Person]>>()
.flatMapLatest {
return fetchPeople().asObservable().materialize()
}
.share()
// this is bound to a search field
let filterText = PublishSubject<String>()
let filteredPeople = Observable.combineLatest(people, filterText) { peopleEvent, filter in
// this is the problem. the completed event from people is being returned, and it terminates the stream
guard let people = peopleEvent.element else { return peopleEvent }
if filterText.isEmpty { return .next(people) }
return .next(people.filter { ... })
}
func fetchPeople() -> Single<[Person]> {
return Single<[Person]>.create { observer in
PeopleService.fetch { result in
switch result {
case .success(let people):
observer(.success(people))
case .failure(let error):
observer(.error(error))
}
}
return Disposables.create()
}
}
filteredPeople.subscribe(
onNext: { event in
// ?! doesn't get called
},
onCompleted: {
// we get here, but why?
},
onError: {event in
...
}).disposed(by: disposeBag)
You haven't posted the code that is causing the problem. The code below works as expected:
struct Person { }
class PeopleService {
static func fetch(_ result: #escaping (Result<[Person], Error>) -> Void) {
result(.success([]))
}
}
let disposeBag = DisposeBag()
func fetchPeople() -> Single<[Person]> {
return Single<[Person]>.create { observer in
PeopleService.fetch { result in
switch result {
case .success(let people):
observer(.success(people))
case .failure(let error):
observer(.error(error))
}
}
return Disposables.create()
}
}
let people = Observable<Void>.just(())
.flatMapLatest { _ in
return fetchPeople().asObservable().materialize()
}
.share()
people.subscribe(
onNext: { event in
print("onNext does get called")
print("in fact, it will get called twice, once with a .next(.next([Person])) event")
print("and once with a .next(.completed) event.")
},
onCompleted: {
print("this prints after onNext gets called")
})
.disposed(by: disposeBag)
I fixed it by filtering out completed events from the inner stream. I am not sure this is the right way, but I can't think of a better solution.
let people = Observable<Event<[Person]>>()
.flatMapLatest {
return fetchPeople()
.asObservable()
.materialize()
// Our work is done, but don't end the parent stream
.filter { !$0.isCompleted }
}
.share()

Observable.CombineLatest bind and subscribe RxSwift

I am working on an app that uses an API that have some inconsistencies, I have achieved a result with these 2 observables that perform some shared actions but the first one 'servers' is an array that binds to the UITableView.
serversViewModel.servers
.asObservable()
.observeOn(MainScheduler.instance)
.bind(to: serversTableView.rx.items(cellIdentifier: ServersTableViewCell.identifier, cellType: ServersTableViewCell.self)) { [weak self] (row, element, cell) in
guard let strongSelf = self else { return }
cell.serverProxy.accept(element)
if let currentServer = strongSelf.serversViewModel.currentServer.value,
element == currentServer,
let index = strongSelf.serversViewModel.servers.value.firstIndex(where: { $0 == currentServer }){
strongSelf.serversTableView.selectRow(at: IndexPath(row: index, section: 0), animated: true, scrollPosition: .top)
}
}
.disposed(by: disposeBag)
serversViewModel.currentServer
.asObservable()
.observeOn(MainScheduler.instance)
.subscribe(onNext: { [weak self] (server) in
guard let strongSelf = self else { return }
if let server = server, let index = strongSelf.serversViewModel.servers.value.firstIndex(where: { $0 == server }){
strongSelf.serversTableView.selectRow(at: IndexPath(row: index, section: 0), animated: true, scrollPosition: .top)
}
else{
strongSelf.serversTableView.deselectAllItems(animated: false)
}
})
.disposed(by: disposeBag)
Is it possible to create a combined observable for both and use it for binding the UITableView?
Thank you
You want to use combineLatest. Note that most of this should actually be in your view model...
In the below code, the servers constant is a tuple of both the array of Server objects that should be displayed and the index path of the current server. Whenever either emits a new value, servers will emit a value.
You might find the following article helpful in the future: Recipes for Combining Observables in RxSwift
let servers = Observable.combineLatest(serversViewModel.servers, serversViewModel.currentServer) { (servers, server) -> ([Server], IndexPath?) in
let indexPath = server.flatMap { servers.firstIndex(of: $0) }
.map { IndexPath(row: $0, section: 0) }
return (servers, indexPath)
}
servers
.map { $0.0 }
.bind(to: serversTableView.rx.items(cellIdentifier: ServersTableViewCell.identifier, cellType: ServersTableViewCell.self)) { (row, element, cell) in
cell.serverProxy.accept(element)
}
.disposed(by: disposeBag)
servers
.map { $0.1 }
.bind(onNext: { [serversTableView] indexPath in
if let indexPath = indexPath {
serversTableView.selectRow(at: indexPath, animated: true, scrollPosition: .top)
}
else {
serversTableView.deselectAllItems(animated: false)
}
})
.disposed(by: disposeBag)
I'd approach from a slightly different way. First I would consider pulling the combine observable back into the ViewModel you have already structured. No need for this composition to be in your ViewController.
Then output that composed signal to bind into your rx.items.
You can wrap your objects will a table cell view model to control whether to show them in a 'selected state'
Then also output the currentServer from your viewModel to simply scroll to it.
There are several ways to combine observables in RxSwift. For your specific case, you will have to choose one that suits your needs best. Some of the operators are:
combineLatest
zip
merge
etc.
Read this documentation to get more idea about what each one it does.

Mapping of each emit -- SwitchMap guaranteeing atleast 1 emit / ConcatMap hybrid?

im breaking my mind around how to do this in RX.
The actual usecase is mapping of LowerLevelEvent(val userId: String) to HigherLevelEvent(val user: User), where the User is provided by observable, so it can emit n times, so example output
LowerLevelEvent1(abc) -> HigherLevelEvent1(userAbc(nameVariation1)
LowerLevelEvent2(abc) -> HigherLevelEvent2(userAbc(nameVariation1)
LowerLevelEvent3(abc) -> HigherLevelEvent3(userAbc(nameVariation1)
LowerLevelEvent4(abc) -> HigherLevelEvent4(userAbc(nameVariation1)
HigherLevelEvent4(userAbc(nameVariation2)
HigherLevelEvent4(userAbc(nameVariation3)
So my naive solution was to use combineLatest. So while userId is not changed user observable is subscribed, i.e. not resubscribed when new lowerLevelEmits & its userId is not changed
val _lowerLevelEventObservable: Observable<LowerLevelEvent> = lowerLevelEventObservable
.replayingShare()
val _higherLevelEventObservable: Observable<HigherLevelEvent> = Observables
.combineLatest(
_lowerLevelEventObservable,
_lowerLevelEventObservable
.map { it.userId }
.distinctUntilChanged()
.switchMap { userRepository.findByIdObservable(it)
) { lowerLevelEvent, user -> createHigherLevelInstance... }
However this has glitch issues, since both sources in combineLatest originate from same observable.
Then I thought about
lowerLevelObservable.
.switchMap { lowerLevelEvent ->
userRepository.findByIdObservable(lowerLevelEvent.userId)
.map { user -> createHigherLevelInstance... }
}
This however can break if lowerLevelObservable emits fast, and since user observable can take some time, given lowerLevelX event can be skipped, which I cannot have. Also it resubscribes user observable each emit, which is wasteful since it wont change most likely
So, maybe concatMap? That has issue of that the user observable doesnt complete, so concatMap wouldnt work.
Anyone have a clue?
Thanks a lot
// Clarification:
basically its mapping of A variants (A1, A2..) to A' variants (A1', A2'..) while attaching a queried object to it, where the query is observable so it might reemit after the mapping was made, so AX' needs to be reemited with new query result. But the query is cold and doesnt complete
So example A1(1) -> A1'(user1), A2(1) -> A2'(user1), A3(1) -> A3'(user1) -- now somebody changes user1 somewhere else in the app, so next emit is A3'(user1')
Based on the comments you have made, the below would work in RxSwift. I have no idea how to translate it to RxJava. Honestly though, I think there is a fundamental misuse of Rx here. Good luck.
How it works: If it's allowed to subscribe it will, otherwise it will add the event to a buffer for later use. It is allowed to subscribe if it currently isn't subscribed to an inner event, or if the inner Observable it's currently subscribed to has emitted an element.
WARNING: It doesn't handle completions properly as it stands. I'll leave that to you as an exercise.
func example(lowerLevelEventObservable: Observable<LowerLevelEvent>, userRepository: UserRepository) {
let higherLevelEventObservable = lowerLevelEventObservable
.flatMapAtLeastOnce { event in // RxSwift's switchLatest I think.
Observable.combineLatest(
Observable.just(event),
userRepository.findByIdObservable(event.userId),
resultSelector: { (lowLevelEvent: $0, user: $1) }
)
}
.map { createHigherLevelInstance($0.lowLevelEvent, $0.user) }
// use higherLevelEventObservable
}
extension ObservableType {
func flatMapAtLeastOnce<U>(from fn: #escaping (E) -> Observable<U>) -> Observable<U> {
return Observable.create { observer in
let disposables = CompositeDisposable()
var nexts: [E] = []
var disposeKey: CompositeDisposable.DisposeKey?
var isAllowedToSubscribe = true
let lock = NSRecursiveLock()
func nextSubscription() {
isAllowedToSubscribe = true
if !nexts.isEmpty {
let e = nexts[0]
nexts.remove(at: 0)
subscribeToInner(e)
}
}
func subscribeToInner(_ element: E) {
isAllowedToSubscribe = false
if let key = disposeKey {
disposables.remove(for: key)
}
let disposable = fn(element).subscribe { innerEvent in
lock.lock(); defer { lock.unlock() }
switch innerEvent {
case .next:
observer.on(innerEvent)
nextSubscription()
case .error:
observer.on(innerEvent)
case .completed:
nextSubscription()
}
}
disposeKey = disposables.insert(disposable)
}
let disposable = self.subscribe { event in
lock.lock(); defer { lock.unlock() }
switch event {
case let .next(element):
if isAllowedToSubscribe == true {
subscribeToInner(element)
}
else {
nexts.append(element)
}
case let .error(error):
observer.onError(error)
case .completed:
observer.onCompleted()
}
}
_ = disposables.insert(disposable)
return disposables
}
}
}

Resources