How to use the when function in Promisekit loop - swift

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)
}
}

Related

Swift for loop not waiting for firestore call to complete

I know firestore calls are async which means this will not work:
private func removeUserSavedMomentFromAllUsers(moment: StoryMoment, completion: #escaping () -> Void) {
guard let savedByUIDs = moment.savedByUIDs else { return }
guard let momentID = moment.id else { return }
for id in savedByUIDs {
self.userInfoCollection.document(id).collection("savedMedias").document(momentID).delete { error in
if let error = error {
print("Error removing user saved moment from UID: \(error)")
}
}
}
}
Since the loop will continue before the delete call completes (same with get requests). I have used dispatch groups in the past to solve this issue. Heres a working example:
private func removeUserSavedMomentFromAllUsers(moment: StoryMoment, completion: #escaping () -> Void) {
guard let savedByUIDs = moment.savedByUIDs else { return }
guard let momentID = moment.id else { return }
let disSemaphore = DispatchSemaphore(value: 0)
let dispatchQueue = DispatchQueue(label: "group 1")
dispatchQueue.async {
for id in savedByUIDs {
self.userInfoCollection.document(id).collection("savedMedias").document(momentID).delete { error in
if let error = error {
print("Error removing user saved moment from UID: \(error)")
} else {
disSemaphore.signal()
}
}
disSemaphore.wait()
}
}
}
But those do all the work on the background thread.
My question is: How can I use async/await in a for loop where you call firebase docs?
The code in the first part of the question does work - and works fine for small group of data. However, in general it's recommended to not call Firebase functions in tight loops.
While the question mentions DispatchQueues, we use DispatchGroups with .enter and .leave as it's pretty clean.
Given a Firebase structure
sample_data
a
key: "value"
b
key: "value"
c
key: "value"
d
key: "value"
e
key: "value"
f
key: "value"
and suppose we want to delete the d, e, and f documents. Here's the code
func dispatchGroupDelete() {
let documentsToDelete = ["d", "e", "f"]
let collection = self.db.collection("sample_data") //self.db points to my Firestore
let group = DispatchGroup()
for docId in documentsToDelete {
group.enter()
let docToDelete = collection.document(docId)
docToDelete.delete()
group.leave()
}
}
While this answer doesn't use async/await, those may not be needed for this use case
If you want to use async/await you try do this
let documentsToDelete = ["d", "e", "f"]
let collection = self.db.collection("sample_data")
for docId in documentsToDelete {
let docToDelete = collection.document(docId)
Task {
do {
try await docToDelete.delete()
} catch {
print("oops")
}
}
}

Firebase Storage listAll() body not executed

I am new to Firebase and Swift. My previous question was very vague due to a misunderstanding on my part. In a class named "A" for example I am trying to create an object of class "B" that contains the fetchARImageTargets function that I have below. I am trying to assign the array ARImageTargets to a var in class "A" however, the listAll completion is not returned in time, which results in the var being empty. Is there a way that I can edit my function or class to avoid the var being set prematurely?
let ARImageTargetStorageRef = Storage.storage().reference().child("ImageTargets")
self.fetchARImageTargets(ref: ARImageTargetStorageRef)
func fetchARImageTargets(ref: StorageReference) {
ref.listAll { (result, error) in
if let error = error {
print(error)
}
for prefix in result.prefixes {
self.fetchARImageTargets(ref: prefix)
}
for item in result.items {
item.getMetadata { (metadata, error) in
if let error = error {
print(error)
} else {
var imageTarget = ARImageTarget()
item.downloadURL(completion: { (url, error) in
imageTarget.ImageURL = url
})
imageTarget.Id = metadata?.customMetadata?["Id"] as String?
let width = metadata?.customMetadata?["PhysicalWidth"] as String?
imageTarget.PhysicalWidth = CGFloat(truncating: NumberFormatter().number(from: width!)!)
self.ARImageTargets.append(imageTarget)
}
}
}
}
}

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.

What is the main difference between the "return function" and the "callback function"?

I think I am going to change all of my code functions from "callback functions" to "return functions". I don't like the "stairs" look of my code.
Do you think it is a good idea?
I don't understand the difference between the two (except for the asynchronous web service calls that force the use of the callback function in my code).
Callback function:
Declaration:
func methodToSelectData(strQuery : String, dataBase: String, completion: #escaping (_ result: [AnyObject]) -> Void) {
let arryToReturn : [AnyObject] = []
let contactDB = FMDatabase(path: String(methodToCreateDatabase(dataBase: dataBase)!.absoluteString) )
if (contactDB?.open())! {
let results:FMResultSet? = contactDB?.executeQuery(strQuery, withArgumentsIn: nil)
while results?.next() == true {
arryToReturn.add(results!.resultDictionary())
}
if arryToReturn.count == 0 {
completion(arryToReturn)
}
contactDB?.close()
} else {
print("Error: \(String(describing: contactDB?.lastErrorMessage()))")
}
completion(arryToReturn)
}
Usage:
DBHandler.sharedInstance.methodToSelectData(strQuery:"SELECT * FROM table", dataBase: "DB.db", completion: { resultQuery in
if (resultQuery.count > 0) {
...
}
})
Return function
Declaration:
func method2ToSelectData(strQuery : String, dataBase: String) -> [AnyObject] {
let arryToReturn : [AnyObject] = []
let contactDB = FMDatabase(path: String(methodToCreateDatabase(dataBase: dataBase)!.absoluteString) )
if (contactDB?.open())! {
let results:FMResultSet? = contactDB?.executeQuery(strQuery, withArgumentsIn: nil)
while results?.next() == true {
arryToReturn.add(results!.resultDictionary())
}
if arryToReturn.count == 0 {
return arryToReturn
}
contactDB?.close()
} else {
print("Error: \(String(describing: contactDB?.lastErrorMessage()))")
}
return arryToReturn
}
Usage:
let resultQuery = DBHandler.sharedInstance.method2ToSelectData(strQuery:"SELECT * FROM table", dataBase: "DB.db")
if (resultQuery.count > 0) {
...
}
What is the best way to use one or the other? I don't understand the subtlety very well.
It's really a matter of what you need in any given situation.
For something as simple as returning a piece of data, you can do just that:
// Definition //
func newString(firstHalf: String, secondHalf: String) -> String {
return firstHalf + secondHalf
}
// Usage //
print(newString(firstHalf: "Hello", secondHalf: "world"))
Something more complicated, like a data call, might need a completion handler or closure:
// Definition //
func getData(fromEndpoint endpoint: String, completion: (String) -> Void) {
let data = serverData(from: endpoint) //Makes the server request.
completion(data)
}
// Usage //
getData(fromEndpoint: "https://www.reddit.com/.json") { data in
doThings(with: data)
}
You don't necessarily need an asynchronous call to use a closure/callback, but it tends to be one of the most common use-cases for one. As you do more coding in Swift, you'll find more use-cases for each.

RxSwift request for each iteration of array

I'm using RxSwift to fetch some network data and I'm having trouble with performing a request for each iteration of an array. This was my idea:
I have an API endpoint that returns an array of Objs which doesn't contain location data. Then I would loop through the array of Objs and for each get the location details with the Obj id. Something like this:
(code simplified)
var arrayObj = networkClient.request(getObjsEndpoint)
.fetchObjLocationDetails(withNetworkClient: networkClient)
And the fetchObjLocationDetails() would be something like:
(code simplified)
extension ObservableType where E == [Obj]? {
func fetchObjsLocationDetails(withNetworkClient networkClient: NetworkClient) -> Observable<[Obj]?> {
return flatMap { Objs -> Observable<[Obj]?> in
guard let unwrappedObjs = Objs as [Obj]? else { return Observable.just(nil) }
let disposeBag = DisposeBag()
var populatedObjs = [Obj]()
unwrappedObjs.forEach { obj in
let getLocationDetailsEndpoint = WeDriveParkAPI.getLocation(id: String(obj.id))
networkClient.request(getLocationDetailsEndpoint)
.observeOn(MainScheduler.instance)
.subscribe(onNext: { json in
guard let populatedObj = Obj.fromJSON(json) as Obj? else { return }
populatedObjs += [populatedObj]
}, onError:{ e in
}).addDisposableTo(disposeBag)
}
return Observable.just(populatedObjs)
}
}
}
This solution is not really working because the code doesn't even go inside the subscribe next closure.
Please have in mind I'm new to both Swift and RxSwift programming so be gentle :) Any help would be greatly appreciated.
Instead of making custom operator you can use built-in.
networkClient.request(getObjsEndpoint)
.map({ (objs:[Obj]?) -> [Obj] in
if let objs = objs {
return objs
} else {
throw NSError(domain: "Objs is nil", code: 1, userInfo: nil)
}
})
.flatMap({ (objs:[Obj]) -> Observable<[Obj]> in
return objs.toObservable().flatMap({ (obj:Obj) -> Observable<Obj> in
let getLocationDetailsEndpoint = WeDriveParkAPI.getLocation(id: String(obj.id))
return self.networkClient.request(getLocationDetailsEndpoint)
.map({ (obj:Obj?) -> Obj in
if let obj = obj {
return obj
} else {
throw NSError(domain: "Obj is nil", code: 1, userInfo: nil)
}
})
}).toArray()
})
.subscribeNext({ (objs:[Obj]) in
print("Populated objects:")
print(objs)
}).addDisposableTo(bag)