Closures for waiting data from CloudKit - swift

I have a CloudKit database with some data. By pressing a button my app should check for existence of some data in the Database. The problem is that all processes end before my app get the results of its search. I found this useful Answer, where it is said to use Closures.
I tried to follow the same structure but Swift asks me for parameters and I get lost very quick here.
Does someone can please help me? Thanks for any help
func reloadTable() {
self.timePickerView.reloadAllComponents()
}
func getDataFromCloud(completionHandler: #escaping (_ records: [CKRecord]) -> Void) {
print("I begin asking process")
var listOfDates: [CKRecord] = []
let predicate = NSPredicate(value: true)
let query = CKQuery(recordType: "Riservazioni", predicate: predicate)
let queryOperation = CKQueryOperation(query: query)
queryOperation.resultsLimit = 20
queryOperation.recordFetchedBlock = { record in
listOfDates.append(record)
}
queryOperation.queryCompletionBlock = { cursor, error in
if error != nil {
print("error")
print(error!.localizedDescription)
} else {
print("NO error")
self.Array = listOfDates
completionHandler(listOfDates)
}
}
}
var Array = [CKRecord]()
func generateHourArray() {
print("generate array")
for hour in disponibleHours {
let instance = CKRecord(recordType: orderNumber+hour)
if Array.contains(instance) {
disponibleHours.remove(at: disponibleHours.index(of: hour)!)
}
}
}
func loadData() {
timePickerView.reloadAllComponents()
timePickerView.isHidden = false
}
#IBAction func checkDisponibility(_ sender: Any) {
if self.timePickerView.isHidden == true {
getDataFromCloud{ (records) in
print("gotData")
self.generateHourArray()
self.loadData()
}
print(Array)
}
}

Im struggling to understand your code and where the CloudKit elements fit in to it, so Im going to try and give a generic answer which will hopefully still help you.
Lets start with the function we are going to call to get our CloudKit data, lets say we are fetching a list of people.
func getPeople() {
}
This is simple enough so far, so now lets add the CloudKit code.
func getPeople() {
var listOfPeople: [CKRecord] = [] // A place to store the items as we get them
let query = CKQuery(recordType: "Person", predicate: NSPredicate(value: true))
let queryOperation = CKQueryOperation(query: query)
queryOperation.resultsLimit = 20
// As we get each record, lets store them in the array
queryOperation.recordFetchedBlock = { record in
listOfPeople.append(record)
}
// Have another closure for when the download is complete
queryOperation.queryCompletionBlock = { cursor, error in
if error != nil {
print(error!.localizedDescription)
} else {
// We are done, we will come back to this
}
}
}
Now we have our list of people, but we want to return this once CloudKit is done. As you rightly said, we want to use a closure for this. Lets add one to the function definition.
func getPeople(completionHandler: #escaping (_ records: [CKRecord]) -> Void) {
...
}
This above adds a completion hander closure. The parameters that we are going to pass to the caller are the records, so we add that into the definition. We dont expect anyone to respond to our completion handler, so we expect a return value of Void. You may want a boolean value here as a success message, but this is entirely project dependent.
Now lets tie the whole thing together. On the line I said we would come back to, you can now replace the comment with:
completionHandler(listOfPeople)
This will then send the list of people to the caller as soon as CloudKit is finished. Ive shown an example below of someone calling this function.
getPeople { (records) in
// This code wont run until cloudkit is finished fetching the data!
}
Something to bare in mind, is which thread the CloudKit API runs on. If it runs on a background thread, then the callback will also be on the background thread - so make sure you don't do any UI changes in the completion handler (or move it to the main thread).
There are lots of improvements you could make to this code, and adapt it to your own project, but it should give you a start. Right off the bat, Id image you will want to change the completion handler parameters to a Bool to show whether the data is present or not.
Let me know if you notice any mistakes, or need a little more help.

Related

Why is My Code running this way and is There a better way to solve this issue

// In the code below I am trying to return an array from data in firestore, the array always returned empty when I put the handler outside the for loop so I had to use an if statement inside the for loop to get the array containing the data. after using the print statement you see in the code i found out that the compiler is going over the entire function before entering the for loop, (print("5") & (print("6") are the first to run and when I put the handler outside the for it will also be triggered and return an empty array
**
func getMyGames(joinedGamesIDs: [String], handler: #escaping(_ games: [GameViewModal]) -> ()) {
var games = [GameViewModal]()
if !joinedGamesIDs.isEmpty{
for id in joinedGamesIDs {
db.collection("games").document(id).getDocument { (document, error) in
if let document = document, document.exists {
if let game = self.getGameViewModal(document: document){
games.append(game)
print("1")
print(games.count)
}
print("2")
print(games.count)
}
print("3")
print(games.count)
if games.count == (joinedGamesIDs.count){
handler(games)
}
print("4")
print(games.count)
}
}
print("5")
print(games.count)
}
print("6")
print(games.count)
}
**
I've embedded my explanations in the code commentary for easier reading. But the problem you have is that you aren't coordinating these async tasks (the getting of each document). You must coordinate them so when the last one finishes, you can "return" the array from the function. This function doesn't technically "return" anything (except Void) but the completion handler, in a way, "returns" the array which is why I put it in quotes. These semantic details matter and it helps to understand everything better.
func getMyGames(joinedGamesIDs: [String], handler: #escaping (_ games: [GameViewModel]) -> ()) {
guard !joinedGamesIDs.isEmpty else {
// If there is nothing to do, always consider
// calling the handler anyway, with an empty
// array, so the caller isn't left hanging.
return handler([])
}
// Set up a Dispatch Group to coordinate the multiple
// async tasks. Instatiate outside of the loop.
let group = DispatchGroup()
var games: [GameViewModel] = []
for id in joinedGamesIDs {
// Enter the group on each iteration of async work
// to be performed.
group.enter()
db.collection("games").document(id).getDocument { (document, error) in
if let doc = document,
doc.exists,
let game = self.getGameViewModal(document: doc) {
games.append(game)
} else if let error = error {
// Always print errors when in development.
print(error)
}
// No matter what happens inside the iteration,
// whether there was a success in getting the
// document or a failure, always leave the group.
group.leave()
}
}
// Once all of the calls to enter the group are equalled
// by the calls to leave the group, this block is called,
// which is the group's own completion handler. Here is
// where you ultimately call the function's handler and
// return the array.
group.notify(queue: .main) {
handler(games)
}
}

Why am I still able to fetch data, even with deleting FireStore object in Swift?

I deleted an entry in the Firestore and also checked it manually to confirm that. However, as long as I do not close the application, I can send a request to fetch the data and I still get the result. This should not be the case.
If you imagine having a shared photo with some textual information and you delete those information, this would mean, other users can still see the textual information (fetched from the Firestore) but not the image anymore (store in Firestorage).
I want to display a message on the UI, something like "The content does not exist anymore".
How I can achieve that? I used the following approach so far but it does not work at the moment:
public func checkIfChallengeObjectExists(completionHandler:#escaping(Bool)->(), challengeId:String) {
CHALLENGE_COLLECTION?.document(challengeId).getDocument(completion: { (querySnapshot, error) in
if (error != nil) {
print(error?.localizedDescription as Any)
}
if (querySnapshot?.documentID == "" || querySnapshot!.metadata.isFromCache) {
completionHandler(false)
}
else {
completionHandler(true)
}
})
}
Any solutions?
Non-existent documents will still return document snapshots, but they will be empty. Therefore, you must check the contents of the snapshot for the document, not the snapshot itself. Also, you should handle errors and the overall flow of the return better.
public func checkIfChallengeObjectExists(completionHandler:#escaping(Bool)->(), challengeId:String) {
CHALLENGE_COLLECTION?.document(challengeId).getDocument(completion: { (querySnapshot, error) in
if let doc = querySnapshot,
doc.exists {
completionHandler(true) // only one possible true condition
} else {
if let error = error {
print(error.localizedDescription)
}
completionHandler(false) // all else false
}
})
}
As a side note, I recommend reordering the parameters of the function to make it easier to read when called (conventionally, the completion handler comes last) and giving the boolean argument a name so it's easier to read when referencing (sometime later or by other developers).
public func verifyChallengeObject(ID: String, _ completion: #escaping (_ exists: Bool) -> Void) {
...
}
verifyChallengeObject(ID: "abc123", { (exists) in
if exists {
...
} else {
...
}
})

addDocument completion handler is never called

I have a function adding documents to a collection in firebase. It is done using a for loop. I have a DispatchGroup and I am calling enter() at the start of each iteration of the loop. After each document has been added I want to call the completion handler of the addDocument method. In the completion handler I want to call leave() on my DispatchGroup, so that I eventually can perform a segue when all documents have been added. My problem is that the completion handler never seems to get called as the messages never get printed. I can see that the documents get added to my collection in firebase every time I run the code. Have I misunderstood something or is there something wrong with my approach? Any help would be very appreciated. A simplified example of my code looks something like this:
func uploadDocumentToFirebase(names: String[])
{
for name in names
{
dispatchGroup.enter()
collection.addDocument(data: ["name": name], completion: {error in
print("Document: \(name) was uploaded to firebase")
self.dispatchGroup.leave()
})
}
}
The actual documents I'm adding have 6 fields instead of the 1 shown in my example, if that makes any difference.
There are many ways to do this - here's two. First is using a dispatch group and the second is using and index technique.
I have an array of words and want to write them to Firestore, notifying as each one is written and then when they are all written.
let arrayOfWords = ["boundless", "delicious", "use", "two", "describe", "hilarious"]
Here's the dispatch group code. We enter the group, write the data and in the completion handler, when done, leave. When all have been left group.notify is called.
func writeWordUsingDispatchGroup() {
let group = DispatchGroup()
let wordCollection = self.db.collection("word_collection")
for word in self.arrayOfWords {
group.enter()
let dataToWrite = ["word": word]
wordCollection.addDocument(data: dataToWrite, completion: { error in
print("\(word) written")
group.leave()
})
}
group.notify(queue: .main) {
print("all words written")
}
}
And then the index code. All this does is calculates the index of the last object in the array and then iterates over the array enumerated (so we get the index). Then when the index of the current loop matches the last index, we know we're done.
func writeWordsUsingIndex() {
let wordCollection = self.db.collection("word_collection")
let lastIndex = self.arrayOfWords.count - 1
for (index, word) in self.arrayOfWords.enumerated() {
let dataToWrite = ["word": word]
wordCollection.addDocument(data: dataToWrite, completion: { error in
print("\(word) written")
if index == lastIndex {
print("all words written")
}
})
}
}
Edit:
Maybe you can run a completion handler so that your exits are in the same place as your group? I generally write completion handlers in situations like this this and call them where you have self.dispatchGroup.leave(). You can put self.dispatchGroup.leave() in the completion block which might help? It seems like your group has an uneven number of entry points and exit points. Organizing with a completion block might help find it?
completion: (#escaping (Bool) -> ()) = { (arg) in })
Original:
Would you mind using this setData code instead of addDcoument to see if it helps? You can add your dispatch to this code and see if it all works. If not I will keep thinking it through...
Also maybe check to make sure the input array isn't empty (just print it to console in the method).
let db = Firestore.firestore()
db.collection("your path").document("\(your document name)").setData([
"aName": "\(name)",
"anEmail": "\(email)",
]) { err in
if let _ = err {
print("Error writing document:")
} else {
print("Document successfully written!")
}
}
My problem is that the completion handler never seems to get called as
the messages never get printed.
It seems you're not calling completion inside your addDocument method, at least for the case, when a document is successfully added.

Is there a better way to optimise this Firestore Query?

Right now I query Firestore arrays based on another array of strings. This works fine but I would like to know if this is correct and if there is a way to optimise it to make it faster.
Here is my code.
var exampleArray = [test]()
func loadData(textArray : [String]){
let db = Firestore.firestore()
for i in 0..<textArray.count{
db.collection("testCollection").whereField("testField", arrayContains: textArray[i]).getDocuments{ (querySnapshot, err) in
if let err = err {
print("\(err.localizedDescription)")
print("Test Error")
} else {
if (querySnapshot!.isEmpty == false){
let res = querySnapshot!.documents.compactMap({test(dictionary: $0.data())})
self.exampleArray.append(contentsOf: res)
self.summaryTableView.reloadData()
SVProgressHUD.dismiss()
print(self.textArray[i])
}
}
}
}
}
Thanks in advance.
UPDATE:
Some info:
Right now the VisionCloudRecognition process and query take about 10-11sec. 7 sec the recognition and 3-4 the query.
The Database has 7700 documents, 7 fields each.
Basically my app does the following:
User takes a photo
With OCR the image becomes text and i append the text in an array of strings.We don't know how many strings are going to be. Maybe 0 or maybe 50.
Then i query Firestore arrays based on the array of strings.
As pointed out in the comments, you're running a bunch of async requests in a loop (that's always bad) and you're reloading the data as each one resolves causing the tableView to keep repainting with new data.
What you want to do is add all of those db requests to a DispatchGroup. As they resolve, add the data to your array. Once they have all finished, do a single tableView.reloadData() and dismiss your SVProgressHud.
This should not only speed things up because multiple calls can happen in parallel, but your user will not have table rows bouncing around as new data is added and the tableView reloads X number of times in rapid fire.
EDIT: You asked for an example - this is very quick and written here on SO, so look for XCode to notify you of any typos or errors.
class MyVC: UIViewController {
var exampleArray = [test]()
let db = Firestore.firestore()
let dispatchGroup = DispatchGroup()
override func viewDidLoad(animated: bool) {
super.viewDidLoad(animated)
myTableView.delegate = self
myTableView.dataSource = self
for i in 0..<textArray.count {
getTestFieldRecords(i)
}
dispatchGroup.notify(queue: .main) {
self.myTableView.reloadData()
}
}
private func getTestFieldRecords(_ record: Int) {
dispatchGroup.enter()
db.collection("testCollection").whereField("testField", arrayContains: textArray[i]).getDocuments{ (querySnapshot, err) in
if let err = err {
print("\(err.localizedDescription)")
print("Test Error")
} else {
// rest of your code because I'm too lazy to format it
}
self.dispatchGroup.leave()
}
}
}
Make sure you call leave() every time - error or not. The DispatchGroup will not finish until all enter() commands are finished with a leave() command. Hope this helps point you in the right direction!

Convert recursive async function to promise

I have a recursive, async function that queries Google Drive for a file ID using the REST api and a completion handler:
func queryForFileId(query: GTLRDriveQuery_FilesList,
handler: #escaping FileIdCompletionHandler) {
service.executeQuery(query) { ticket, data, error in
if let error = error {
handler(nil, error)
} else {
let list = data as! GTLRDrive_FileList
if let pageToken = list.nextPageToken {
query.pageToken = pageToken
self.queryForFileId(query: query, handler: handler)
} else if let id = list.files?.first?.identifier {
handler(id, nil)
} else {
handler(nil, nil) // no file found
}
}
}
}
Here, query is set up to return the nextPageToken and files(id) fields, service is an instance of GTLRDriveService, and FileIdCompletionHandler is just a typealias:
typealias FileIdCompletionHandler = (String?, Error?) -> Void
I've read how to convert async functions into promises (as in this thread) but I don't see how that can be applied to a recursive, async function. I guess I can just wrap the entire method as a Promise:
private func fileIdPromise(query: GTLRDriveQuery_FilesList) -> Promise<String?> {
return Promise { fulfill, reject in
queryForFileId(query: query) { id, error in
if let error = error {
reject(error)
} else {
fulfill(id)
}
}
}
}
However, I was hoping to something a little more direct:
private func queryForFileId2(query: GTLRDriveQuery_FilesList) -> Promise<String?> {
return Promise { fulfill, reject in
service.executeQuery(query) { ticket, data, error in
if let error = error {
reject(error)
} else {
let list = data as! GTLRDrive_FileList
if let pageToken = list.nextPageToken {
query.pageToken = pageToken
// WHAT DO I DO HERE?
} else if let id = list.files?.first?.identifier {
fulfill(id)
} else {
fulfill(nil) // no file found
}
}
}
}
}
So: what would I do when I need to make another async call to executeQuery?
If you want to satisfy a recursive set of promises, at where your "WHAT DO I DO HERE?" line, you'd create a new promise.then {...}.else {...} pattern, calling fulfill in the then clause and reject in the else clause. Obviously, if no recursive call was needed, though, you'd just fulfill directly.
I don't know the Google API and you didn't share your code for satisfying a promise for a list of files, so I'll have to keep this answer a bit generic: Let's assume you had some retrieveTokens routine that returned a promise that is satisfied only when all of the promises for the all files was done. Let's imagine that the top level call was something like:
retrieveTokens(for: files).then { tokens in
print(tokens)
}.catch { error in
print(error)
}
You'd then have a retrieveTokens that returns a promise that is satisfied only when then promises for the individual files were satisfied. If you were dealing with a simple array of File objects, you might do something like:
func retrieveTokens(for files: [File]) -> Promise<[Any]> {
var fileGenerator = files.makeIterator()
let generator = AnyIterator<Promise<Any>> {
guard let file = fileGenerator.next() else { return nil }
return self.retrieveToken(for: file)
}
return when(fulfilled: generator, concurrently: 1)
}
(I know this isn't what yours looks like, but I need this framework to show my answer to your question below. But it’s useful to encapsulate this “return all promises at a given level” in a single function, as it allows you to keep the recursive code somewhat elegant, without repeating code.)
Then the routine that returns a promise for an individual file would see if a recursive set of promises needed to be returned, and put its fulfill inside the then clause of that new recursively created promise:
func retrieveToken(for file: File) -> Promise<Any> {
return Promise<Any> { fulfill, reject in
service.determineToken(for: file) { token, error in
// if any error, reject
guard let token = token, error == nil else {
reject(error ?? FileError.someError)
return
}
// if I don't have to make recursive call, `fulfill` immediately.
// in my example, I'm going to see if there are subfiles, and if not, `fulfill` immediately.
guard let subfiles = file.subfiles else {
fulfill(token)
return
}
// if I got here, there are subfiles and I'm going to start recursive set of promises
self.retrieveTokens(for: subfiles).then { tokens in
fulfill(tokens)
}.catch { error in
reject(error)
}
}
}
}
Again, I know that the above isn't a direct answer to your question (as I'm not familiar with Google Drive API nor how you did your top level promise logic). So, in my example, I created model objects sufficient for the purposes of the demonstration.
But hopefully it's enough to illustrate the idea behind a recursive set of promises.