Run multiple request at a time and continue as soon there is success - swift

Given an array of urls, is there a way to run those at once simultaneously? But in such a way so as soon as there is one success move to the next request using that successful url.
So far I tried chaining concatMap, and failed with zip.
func updateAccountInfo() -> Single<Bool> {
var disposable: Disposable? = nil
return Single<Bool>.create { observer in
do {
var urls = try self.settings.domains.value()
disposable = Observable.from(urls)
.enumerated()
.concatMap { index, url -> Single<URL> in
return self.verifyUrl(url)
}
.concatMap { url -> Single<Account> in
return self.apiManager.loadAccountInfo(from: url)
}
.observeOn(MainScheduler.instance)
.do(onNext: { (account: AccountInfo) in
// use account unfo here
disposable!.dispose()
})
.subscribe()
} catch {
observer(.error(error))
}
return Disposables.create()
}
}
Tried like so too:
disposable = Observable.from(urls)
.enumerated()
.concatMap { index, url -> Single<(Bool, URL)> in
return self.verifyUrl(url)
}
.subscribe(onNext: { reachable, url in
if reachable {
self.apiManager.loadAccountInfo(from: url)
.subscribe(onSuccess: { accountInfo in
// use account info here
}, onError: { error in
})
.disposed(by: self.bag)
disposable!.dispose()
} else {
}
}, onError: { error in
}, onCompleted: {
})
Maybe I use zip but how would I create an array of verifyUrl(url) calls? Does zip accept arrays of Observable at all?
let obs = Observable.from(urls)
.enumerated()
.concatMap { index, url -> Single<URL> in
return self.verifyUrl(url)
}
let test = Observable
.zip(obs).map { [urls] in
return [urls]
}

If I understand the question correctly, you are looking for something like this:
func example() throws {
let urls = try self.settings.domains.value()
Observable.merge(urls.map { verifyUrl($0).asObservable() })
.flatMap { [apiManager] url in
apiManager!.loadAccountInfo(from: url)
}
.observe(on: MainScheduler.instance)
.subscribe(onNext: { account in
// use account unfo here
})
.disposed(by: self.disposeBag)
}
But it's hard to tell. Your code samples are a bit jumbled. Putting all your code in a Single.create is odd. Returning a Disposables.create() from the closure when you have a disposable to return is odd. Calling dispose() on a disposable inside the do block is odd. So much weirdness... I suggest you post some code on https://codereview.stackexchange.com or look at sample code.

Related

Future Combine sink does not recieve any values

I want to add a value to Firestore. When finished I want to return the added value. The value does get added to Firestore successfully. However, the value does not go through sink.
This is the function that does not work:
func createPremium(user id: String, isPremium: Bool) -> AnyPublisher<Bool,Never> {
let dic = ["premium":isPremium]
return Future<Bool,Never> { promise in
self.db.collection(self.dbName).document(id).setData(dic, merge: true) { error in
if let error = error {
print(error.localizedDescription)
} else {
/// does get called
promise(.success(isPremium))
}
}
}.eraseToAnyPublisher()
}
I made a test function that works:
func test() -> AnyPublisher<Bool,Never> {
return Future<Bool,Never> { promise in
promise(.success(true))
}.eraseToAnyPublisher()
}
premiumRepository.createPremium(user: userID ?? "1234", isPremium: true)
.sink { receivedValue in
/// does not get called
print(receivedValue)
}.cancel()
test()
.sink { recievedValue in
/// does get called
print("Test", recievedValue)
}.cancel()
Also I have a similar code snippet that works:
func loadExercises(category: Category) -> AnyPublisher<[Exercise], Error> {
let document = store.collection(category.rawValue)
return Future<[Exercise], Error> { promise in
document.getDocuments { documents, error in
if let error = error {
promise(.failure(error))
} else if let documents = documents {
var exercises = [Exercise]()
for document in documents.documents {
do {
let decoded = try FirestoreDecoder().decode(Exercise.self, from: document.data())
exercises.append(decoded)
} catch let error {
promise(.failure(error))
}
}
promise(.success(exercises))
}
}
}.eraseToAnyPublisher()
}
I tried to add a buffer but it did not lead to success.
Try to change/remove .cancel() method on your subscriptions. Seems you subscribe to the publisher, and then immediately cancel the subscription. The better option is to retain and store all your subscriptions in the cancellable set.

Refresh Observable in response to another

I have an observable that emits a list of CNContacts, and I want to reload the list when there is a change to the Contacts database (.CNContactStoreDidChange).
So the observable should emit a value on subscription, and whenever the other observable (the notification) emits a value. That sounds like combining them with withLatestFrom, but it doesn't emit anything.
let myContactKeys = [
CNContactIdentifierKey as CNKeyDescriptor,
CNContactFormatter.descriptorForRequiredKeys(for: .fullName)
]
func fetchContacts(by identifiers: [String],
contactKeys: [CNKeyDescriptor]) -> Observable<Event<[CNContact]>> {
return Observable<[String]>.just(identifiers)
.withLatestFrom(NotificationCenter.default.rx.notification(Notification.Name.CNContactStoreDidChange)) { ids, _ in ids}
.flatMap { ids in
Observable<[CNContact]>.create { observer in
let predicate = CNContact.predicateForContacts(withIdentifiers: ids)
do {
let contacts = try CNContactStore().unifiedContacts(matching: predicate, keysToFetch: contactKeys)
observer.onNext(contacts)
} catch {
observer.onError(error)
}
return Disposables.create()
}
.materialize()
}
.observeOn(MainScheduler.instance)
.share(replay: 1)
.debug()
}
fetchContacts(by: ["123"], contactKeys: myContactKeys)
.subscribe(
onNext: { contacts in
contacts.forEach { print($0.fullName) }
},
onError: { error in
print(error.localizedDescription)
})
.dispose(by: disposeBag)
The problem with your code is that you are starting with Observable<[String]>.just(identifiers) which will emit your identifiers and immediately complete. You don't want it to complete, you want it to continue to emit values whenever the notification comes in.
From your description, it sounds like you want something like the below. It emits whenever the notification fires, and starts with the contacts.
let myContactKeys = [
CNContactIdentifierKey as CNKeyDescriptor,
CNContactFormatter.descriptorForRequiredKeys(for: .fullName)
]
func fetchContacts(by identifiers: [String], contactKeys: [CNKeyDescriptor]) -> Observable<Event<[CNContact]>> {
func update() throws -> [CNContact] {
let predicate = CNContact.predicateForContacts(withIdentifiers: identifiers)
return try CNContactStore().unifiedContacts(matching: predicate, keysToFetch: contactKeys)
}
return Observable.deferred {
NotificationCenter.default.rx.notification(Notification.Name.CNContactStoreDidChange)
.map { _ in }
.map(update)
.materialize()
}
.startWith({ () -> Event<[CNContact]> in
do {
return Event.next(try update())
}
catch {
return Event.error(error)
}
}())
.share(replay: 1)
.debug()
}

RxSwift Chaining requests

The problem I faced with is chaining 2 requests and handling errors.
Below an example of my code:
func fbLogin() -> Observable<String> { ... }
func userLogin(request: Request) -> Observable<User> { ... }
let signedWithLogin = loginTaps
.asDriver(onErrorJustReturn: ())
.flatMapLatest { _ in
return fbLogin()
.map({ ReqestState<String>.loaded($0) })
.asDriver(onErrorRecover: { error in
return Driver.just(.error(error))
})
.startWith(.loading)
}
.map({ UserEndpoint.socialLogin(token: $0) })
.flatMapLatest { request in
return userLogin(request: request)
.map({ ReqestState<User>.loaded($0) })
.asDriver(onErrorRecover: { error in
return Driver.just(.error(error))
})
.startWith(.loading)
}
signedWithLogin
.drive(onNext: { response in
print(response)
})
.disposed(by: disposeBag)
Problem is when I cancel the facebook login popup I send observer.onError(FBLoginManagerError.canceled) error. This error catch first .asDriver(onErrorRecover: { error method but does't pass to the .drive(onNext: { responsemethod.
How can I catch all errors in .asDriver(onErrorRecover: { error method?
Mukesh is correct that you probably should avoid Driver until the end. Also, there's little point of having both RequestState types when you only really care about the final one (RequestState<User>)
Here's a simpler version that I think will do what you want:
let signedWithLogin = loginTaps
.flatMapLatest {
fbLogin()
.map { UserEndpoint.socialLogin(token: $0) }
.flatMap { userLogin(request: $0) }
.map { RequestState.loaded($0) }
.catchError { .just(.error($0)) }
.startWith(.loading)
}
.asDriver(onErrorRecover: { fatalError($0.localizedDescription) }) // I'm using `fatalError()` here because if the above emits an error something has gone horribly wrong (like the RxSwift library isn't working anymore.)
signedWithLogin
.drive(onNext: { response in
print(response)
})
.disposed(by: disposeBag)
The above assumes you change your UserEndpoint.socialLogin(token:) function to accept a String instead of a RequestState<String>.
It also assumes that fbLogin() and userLogin(request:) only emit one value each. You might want to consider switching them to Singles.

PromiseKit firstly around code, not function call

I don't want to write a separate function to return a Promise in my firstly call. I just want to write this:
firstly
{
return Promise<Bool>
{ inSeal in
var isOrderHistory = false
let importTester = CSVImporter<String>(url: url)
importTester?.startImportingRecords(structure:
{ (inFieldNames) in
if inFieldNames[2] == "Payment Instrument Type"
{
isOrderHistory = true
}
}, recordMapper: { (inRecords) -> String in
return "" // Don't care
}).onFinish
{ (inItems) in
inSeal.resolve(isOrderHistory)
}
}
}
.then
{ inIsOrderHistory in
if inIsOrderHistory -> Void
{
}
else
{
...
But I'm getting something wrong. ImportMainWindowController.swift:51:5: Ambiguous reference to member 'firstly(execute:)'
None of the example code or docs seems to cover this (what I thought was a) basic use case. In the code above, the CSVImporter operates on a background queue and calls the methods asynchronously (although in order).
I can't figure out what the full type specification should be for Promise or firstly, or what.
According to my understanding, since you are using then in the promise chain, it is also meant to return a promise and hence you are getting this error. If you intend not to return promise from your next step, you can directly use done after firstly.
Use below chain if you want to return Promise from then
firstly {
Promise<Bool> { seal in
print("hello")
seal.fulfill(true)
}
}.then { (response) in
Promise<Bool> { seal in
print(response)
seal.fulfill(true)
}
}.done { _ in
print("done")
}.catch { (error) in
print(error)
}
If you do not want to return Promise from then, you can use chain like below.
firstly {
Promise<Bool> { seal in
print("hello")
seal.fulfill(true)
}
}.done { _ in
print("done")
}.catch { (error) in
print(error)
}
I hope it helped.
Updated:
In case you do not want to return anything and then mandates to return a Promise, you can return Promise<Void> like below.
firstly {
Promise<Bool> { seal in
print("hello")
seal.fulfill(true)
}
}.then { (response) -> Promise<Void> in
print(response)
return Promise()
}.done { _ in
print("done")
}.catch { (error) in
print(error)
}

RxSwift Chaining two signals in right order

So basically I have two actions I need to execute:
first is login
second is get user profile
They have to be done in right order because getting user profile cannot be done without logging in first.
So I had bunch of code that looked like this:
func signIn(signinParameters: SignInParameters) -> Observable<SignInResult> {
return Observable<SignInResult>.create { [unowned self] observer in
self.signinParameters = signinParameters
self.apiConnector
.signIn(with: signinParameters)
.do(onNext: { [weak self] signinResult in
self!.apiConnector
.get()
.do(onNext: { user in
let realm = RealmManager.shared.newRealm()!
let realmUser = RealmUser()
realmUser.configure(with: user, in: realm)
try? realm.write {
realm.add(realmUser, update: true)
}
self!.setState(.authenticated)
observer.onNext(signinResult)
}, onError: { (error) in
observer.onError(error)
}, onCompleted: {
observer.onCompleted()
}).subscribe()
}, onError: { error in
observer.onError(error)
}, onCompleted: {
print("completed")
observer.onCompleted()
}).subscribe()
return Disposables.create()
}
I know this is not right because I cannot send onNext signal with signin result when both actions are finished. I've been reading and I figured out i need to flatmap both actions, combine them into one signal and then manipulate signinresult but I dont have a clue how to do that. So any help would be nice.
Thank you
EDIT 1:
so I've refactored code to look something like this, but there is still problem that I can't send signal when BOTH actions are finished, or am I wrong?
func signIn(signinParameters: SignInParameters) -> Observable<SignInResult> {
return Observable<SignInResult>.create { [unowned self] observer in
self.signinParameters = signinParameters
self.apiConnector
.signIn(with: signinParameters)
.do(onNext: { (result) in
}, onError: { (error) in
}, onCompleted: {
})
.flatMap({ (result) -> Observable<User> in
self.apiConnector.get().asObservable()
})
.do(onNext: { (user) in
}, onError: { (error) in
}, onCompleted: {
}).subscribe()
return Disposables.create()
}
}
Your code is not very clean and it is hard to understand what is going on (my opinion).
If you need two actions to be executed you can create two functions:
struct Parameters{}
struct Profile{}
struct User{}
func login(parameters: Parameters) -> Observable<User> {
// get user
}
func profile(user: User) -> Observable<Profile> {
// get profile
}
func serial(parameters: Parameters) -> Observable<Profile> {
return login(parameters: parameters).flatMap({ profile(user: $0) })
}
login function or profile function can be also split into smaller functions if required:
func profileStored(user: User) -> Observable<Profile?> {
// get stored profile
}
func profileRequested(user: User) -> Observable<Profile> {
// get profile from network
}
func profile(user: User) -> Observable<Profile> {
let observable = profileStored(user: user)
.shareReplayLatestWhileConnected()
let observableStored = observable
.filter({ $0 != nil })
.map({ $0! })
.shareReplayLatestWhileConnected()
let observableRequested = observable
.filter({ $0 == nil })
.flatMap({ _ in profileRequested(user: user) })
.shareReplayLatestWhileConnected()
return Observable
.of(observableStored, observableRequested)
.merge()
.shareReplayLatestWhileConnected()
}
As a result you can mix smaller functions with flatMap or any other operator.
That is how I do it. Hope it'll be helpful