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.
Related
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.
Below there are three functions. The first one is the function that I need to refactor. Basically what I'm hoping for is something similar what can be achieved using Promise Kit but in this case using Swifts combine framework.
The second function loginWithFacebook() returns a AuthCredential.
This AuthCredential needs to be passed on to the last functions which returns a type Future<UserProfileCompact, Error> which is a similar return type to the main function (1st function).
My question is is there a way to achieve this in a Swifty way, similar to Promise Kit doing this operation: return loginWithFacebook().then {loginWithFirebase(:_)}
// Call site is a View Model
// Main Function that needs to be refactored
func loginwithFacebook() -> Future<UserProfileCompact, Error> {
//This returs a Future Firebase Credential
loginWithFacebook()
//The above credential needs to be passed to this method and this returns a type Future<UserProfileCompact, Error>
loginWithFirebase(<#T##credentials: AuthCredential##AuthCredential#>)
}
private func loginWithFacebook() -> Future<AuthCredential,Error> {
return Future { [weak self] promise in
self?.loginManager.logIn(permissions: ["public_profile","email"], from: UIViewController()) { (loginResult, error) in
if let error = error {
promise(.failure(error))
} else if loginResult?.isCancelled ?? false {
//fatalError()
}
else if let authToken = loginResult?.token?.tokenString {
let credentials = FacebookAuthProvider.credential(withAccessToken: authToken)
promise(.success(credentials))
}
else{
fatalError()
}
}
}
}
private func loginWithFirebase(_ credentials: AuthCredential) -> Future<UserProfileCompact, Error> {
return Future { promise in
Auth.auth().signIn(with: credentials) { (result, error) in
if let error = error {
//Crashlytics.crashlytics().record(error: error)
promise(.failure(error))
}
else if let user = result?.user {
//Crashlytics.crashlytics().setUserID(user.uid)
let profile = UserProfileCompactMapper.map(firebaseUser: user)
promise(.success(profile))
}
else {
fatalError()
}
}
}
}
You can use a .flatMap operator, which takes a value from upstream and produces a publisher. This would look something like below.
Note, that it's also better to return a type-erased AnyPublisher at the function boundary, instead of the specific publisher used inside the function
func loginwithFacebook() -> AnyPublisher<UserProfileCompact, Error> {
loginWithFacebook().flatMap { authCredential in
loginWithFirebase(authCredential)
}
.eraseToAnyPublisher()
}
RxSwift one more question about error handling:
I'm using Alamofire+RxAlamofire this way:
SessionManager.default.rx.responseJSON(.post, url, parameters:params)
example:
func login() -> Observable<Int> {
let urlString = ...
let params = ...
return SessionManager.default.rx.responseJSON(.post, url, parameters:params)
.rxJsonDefaultResponse()
.map({ (data) in
data["clientId"] as! Int
})
}
....
extension ObservableType where Element == (HTTPURLResponse, Any) {
func rxJsonDefaultResponse() -> Observable<Dictionary<String, Any>> {
return self.asObservable().map { data -> Dictionary<String, Any> in
if... //error chechings
throw NSError(domain: ..,
code: ...,
userInfo: ...)
}
...
return json
}
}
}
using:
loginBtn.rx.tap
.flatMap{ _ in
provider.login()
}.subscribe(onNext: { id in
...
}, onError: { (er) in
ErrorPresentationHelper.showErrorAlert(for: er)
})
.disposed(by: bag)
So if error occurred everything works as intended: error alert shows and 'loginBtn.rx.tap' disposed, but I need it to be still alive, what's my strategy here if I want to use onError block?
You can use materialize function in rxSwift. It will convert any Observable into an Observable of its events. So that you will be listening to Observable<Event<Int>> than Observable<Int>. Any error thrown from the flatmap would be captured as error event in your subscription block's onNext and can be handled there. And your subscription would still be alive. Sample code would be as follows.
button.rx.tap.flatMap { _ in
return Observable.just(0)
.flatMap { _ -> Observable<Int> in
provider.login()
}.materialize()
}.subscribe(onNext: { event in
switch event {
case .next:
if let value = event.element {
print(value) //You will be getting your value here
}
case .error:
if let error = event.error {
print(error.localizedDescription) //You will be getting your captured error here
}
case .completed:
print("Subscription completed")
}
}) {
print("Subscription disposed")
}.disposed(by: disposeBag)
Hope it helps. You can checkout the materialize extension here.
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()
}
I have an array of appointments and I'm trying to grab all of the photos for these appointments from our windows azure blob storage. First, I want to get the list of blobs with the associated appointmentId so I can download and store them properly afterwards.
I'm using PromiseKit but I'm not at all sure about how to use PromiseKit in a loop:
for appointment in appointments {
// Get blobs
}
Here's my code so far. Any help is greatly appreciated!
func getBlobsPromise(appointmentId: Int32) -> Promise<[BlobDownload]> {
return Promise { seal in
var error: NSError?
var blobDownloads = [BlobDownload]()
container = AZSCloudBlobContainer(url: URL(string: containerURL)!, error: &error)
if ((error) != nil) {
print("Error in creating blob container object. Error code = %ld, error domain = %#, error userinfo = %#", error!.code, error!.domain, error!.userInfo)
seal.reject(error!)
}
let prefix: String = "AppointmentFiles/\(appointmentId)"
container?.listBlobsSegmented(with: nil, prefix: prefix, useFlatBlobListing: true, blobListingDetails: AZSBlobListingDetails(), maxResults: 150) { (error : Error?, results : AZSBlobResultSegment?) -> Void in
if error != nil {
seal.reject(error!)
}
for blob in results!.blobs!
{
let blobInfo = blob as! AZSCloudBlob
if blobInfo.blobName.lowercased().contains("jpg") || blobInfo.blobName.lowercased().contains("jpeg") {
let blobDownload: BlobDownload = BlobDownload(appointmentId: Int(jobId), blob: blobInfo)
blobDownloads.append(blobDownload)
}
}
seal.fulfill(blobDownloads)
}
}
}
That returns the blobs as expected but I want to get all of the blobs for all of the appointments before proceeding. Here's what I tried (among other things):
func getBlobsForAllJobs(appointmentIds: [Int32]) -> Promise<[BlobDownload]> {
return Promise { seal in
let count = appointmentIds.count - 1
let promises = (0..<count).map { index -> Promise<[BlobDownload]> in
return getBlobsPromise(agencyCode: agencyCode, appointmentId: appointmentIds[index])
}
when(fulfilled: promises).then({ blobDownloads in
seal.fulfill(blobDownloads)
})
}
}
EDIT 1
I solved this using a DispatchGroup and completion handler. Here's the code in case someone is interested. If there are alternate (better) ways of doing this I'd love to hear them. I'm a c# guy just getting into Swift.
func getBlobsToDownload(appointmentIds: [Int32], completion: #escaping ([BlobDownload]) -> Void) {
var myBlobsToDownload = [BlobDownload]()
let myGroup = DispatchGroup()
for apptId in appointmentIds {
myGroup.enter()
getBlobs(appointmentId: apptId) { (blobDownloads) in
print("Finished request \(apptId)")
print("Blobs fetched from apptId \(apptId) is \(blobDownloads.count)")
for blobDownload in blobDownloads {
myBlobsToDownload.append(blobDownload)
}
myGroup.leave()
}
}
myGroup.notify(queue: .main) {
print("Finished all requests.")
completion(myBlobsToDownload)
}
}