I'm currently working with Firebase and Firestore and I'm working with their distributed counter system and have created a function with a #escaping handler that should return a value I can assign and then display. here's the code:
func getCount(ref: DocumentReference, handler: #escaping(_ querySnapshot: QuerySnapshot,_ err: Error) -> Void) {
ref.collection("shards").getDocuments() { (querySnapshot, err) in
var totalCount = 0
if err != nil {
print("error getting total count")
} else {
for document in querySnapshot!.documents {
let count = document.data()["count"] as! Int
totalCount += count
}
}
}
}
I'm having trouble calling the function and there isn't too much information that gives me a solid grasp about this. Can someone point me in the right direction?
I call it here:
getCount(ref: Featuredlikes {
ref.collection("shards").getDocuments() { (querySnapshot, err) in
var totalCount = 0
if err != nil {
print("error getting total count")
} else {
for document in querySnapshot!.documents {
let count = document.data()["count"] as! Int
totalCount += count
}
}
}
}
But evidently it isn't right I'm getting since I'm getting unrelated errors in the swiftui part of my code.
Basically your code cannot work because it's impossible to return (strictly spoken the closure does not return anything) a non-optional snapshot and non-optional error simultaneously. Declare both parameters as optional. By the way an underscore character and parameter label in the closure declaration is Swift 2 legacy code.
You have to call handler either with nil snapshot and the error instance or vice versa.
func getCount(ref: DocumentReference, handler: #escaping(QuerySnapshot?, Error?) -> Void) {
ref.collection("shards").getDocuments() { (querySnapshot, err) in
if let error = err {
handler(nil, error)
} else {
handler(querySnapshot, nil)
}
}
}
Or with the modern Result API
func getCount(ref: DocumentReference, handler: #escaping(Result<QuerySnapshot, Error>) -> Void) {
ref.collection("shards").getDocuments() { (querySnapshot, err) in
if let error = err {
handler(.failure(error))
} else {
handler(.success(querySnapshot))
}
}
}
Related
I tried to refactor a Firebase operation from the old completion handler to the new Task.init() and it seems that the operation is now taking longer. Am I doing something wrong? Are the await calls not being done concurrently (which is the reason I am calling both operations at the same time and counting how many finished with the completion handler approach)? Any suggestions for what might be causing the slower execution time?
Thank you in advance.
Completion handler approach (0.73s)
Previously I had this method:
extension Firebase {
static func getAll<T: Decodable>(_ subCollection: SubCollection,
completion: #escaping (_ result: [T]?, _ error: Error?) -> Void) {
db.collection("userData").document(currentUser!.uid).collection("\(subCollection)").getDocuments(completion: { (querySnapshot, error) in
var documents: [T] = []
if let error { print(error.localizedDescription) }
if let querySnapshot {
for document in querySnapshot.documents {
if let decodedDocument = try? document.data(as: T.self) { documents.append(decodedDocument) }
else { print("Failed to decode a retrieved document of type \(T.self) at getAll") }
}
}
completion(documents, error)
AppNotification.post(.firebseOperationCompleted)
})
}
}
Which I would use like this:
class Loading: UIViewController {
var error: Error?
var operationsCompleted = 0
let start = CFAbsoluteTimeGetCurrent()
private func fetchData() {
operationsCompleted = 0
Firebase.getAll(.goals, completion: { (results: [Goal]?, error) in
if let results { UltraDataStorage.goals = results }
if let error { self.error = error }
self.operationsCompleted += 1
})
Firebase.getAll(.ideas, completion: { (results: [Idea]?, error) in
if let results { UltraDataStorage.ideas = results }
if let error { self.error = error }
self.operationsCompleted += 1
})
#objc func advanceWhenOperationsComplete(){
print("Took \(CFAbsoluteTimeGetCurrent() - self.start) seconds")
if operationsCompleted == 2 {
// Proceed if error is nil
}
}
override func viewDidLoad() {
fetchData()
AppNotification.observe(handler: self, name: .firebseOperationCompleted, function: #selector(advanceWhenOperationsComplete))
}
}
Task.init() approach (1.14s)
Now, I updated the getAll function:
extension Firebase {
static func getAll<T: Decodable>(_ subCollection: SubCollection) async -> Result<[T], Error> {
do {
let documents = try await db.collection("userData").document(currentUser!.uid).collection("\(subCollection)").getDocuments()
var decodedDocuments: [T] = []
for document in documents.documents {
if let decodedDocument = try? document.data(as: T.self) { decodedDocuments.append(decodedDocument) }
else { print("Failed to decode a retrieved document of type \(T.self) at getAll") }
}
return.success(decodedDocuments)
}
catch { return.failure(error) }
}
}
And I am now calling it like this
class Loading: UIViewController {
var error: Error?
let start = CFAbsoluteTimeGetCurrent()
private func fetchData() {
Task.init(operation: {
let goalsResult: Result<[Goal], Error> = await Firebase.getAll(.goals)
switch goalsResult {
case .success(let goals): UltraDataStorage.goals = goals
case .failure(let error): self.error = error
}
let ideasResult: Result<[Idea], Error> = await Firebase.getAll(.ideas)
switch ideasResult {
case .success(let ideas): UltraDataStorage.ideas = ideas
case .failure(let error): self.error = error
}
DispatchQueue.main.async {
self.advanceWhenOperationsComplete()
}
})
}
func advanceWhenOperationsComplete(){
print("Took \(CFAbsoluteTimeGetCurrent() - self.start) seconds")
// Proceed when the async operations are completed
}
override func viewDidLoad() {
fetchData()
}
}
The performance difference is likely a result that the completion handler pattern is running the requests concurrently, but the async-await rendition is performing them sequentially. The latter is awaiting the result of the first asynchronous request before even initiating the next asynchronous request.
To get them to run concurrently, you can either use the async let pattern (see SE-0317) or use a task group:
extension Firebase {
static func getAll<T: Decodable>(_ subCollection: SubCollection) async throws -> [T] {
try await db
.collection("userData")
.document(currentUser!.uid)
.collection("\(subCollection)")
.getDocuments()
.documents
.map { try $0.data(as: T.self) }
}
}
// you could use `async let`
private func fetchData1() async throws {
async let goals: [Goal] = Firebase.getAll(.goals)
async let ideas: [Idea] = Firebase.getAll(.ideas)
UltraDataStorage.goals = try await goals
UltraDataStorage.ideas = try await ideas
advanceWhenOperationsComplete()
}
// or task group
private func fetchData2() async throws {
try await withThrowingTaskGroup(of: Void.self) { group in
group.addTask { UltraDataStorage.goals = try await Firebase.getAll(.goals) }
group.addTask { UltraDataStorage.ideas = try await Firebase.getAll(.ideas) }
try await group.waitForAll()
}
advanceWhenOperationsComplete()
}
(These might not be 100% right, as I do not implementations for all of these types and methods and therefore cannot compile this, but hopefully it illustrates the idea. Notably, I am nervous about the thread-safety of UltraDataStorage, especially in the task group example. But, that is beyond the scope of this question.)
Bottom line, async let is an intuitive way to run tasks concurrently and is most useful when dealing with a fixed, limited number of asynchronous tasks. Task groups shine when dealing with a variable number of tasks. That’s not the case here, but I include it for the sake of completeness.
Note, I’ve taken the liberty of excising the Result<[T], Error> type, and instead throw the error.
I am attempting to have my program check for unique usernames. My problem right now is that it will return before completing. I understand since this is async and happening in the background; however, I have attempted using DispatchGroups and semaphores and non of which are working. This is in swift and any help would be appreciated. I am fairly new when it comes to Firestore.
func checkUsernames(_ user: String) {
let docRef = db.collection("userRI").document(user)
docRef.getDocument { (document, error) in
if let document = document, document.exists {
self.uniqueUser = false
print("does exist")
} else {
self.uniqueUser = true
print("does not exist \(self.uniqueUser)")
}
}
}
docRef.getDocument() is asynchronous and returns immediate before the query is complete. The callback will be invoked some time later. That means your function will return before uniqueUser has a value.
The idea to fix is to consider an escaping callback to pass the value around.
func checkUsernames(_ user: String, completion: #escaping (_ uniqueUser: Bool) -> Void) {
let docRef = db.collection("userRI").document(user)
docRef.getDocument { (document, error) in
if let document = document, document.exists {
completion(false)
print("does exist")
} else {
completion(true)
print("does not exist \(self.uniqueUser)")
}
}
}
I am trying to query my Firestore database to see if a desired username is taken. The query works, however I need to return a value if it is empty or not to see if the username already exists. I am trying to use a completion handler but it doesn't seem to work:
func checkUserTaken(cleanUsername: String ,completion:#escaping(String) -> (Void)){
let db = Firestore.firestore()
var userTaken: String = ""
let docRef = db.collection("users").whereField("username", isEqualTo: cleanUsername)
docRef.getDocuments() { (querySnapshot, err) in
if let err = err {
print("Error getting documents: \(err)")
} else {
if(querySnapshot!.documents.isEmpty){
print("user is available")
userTaken = "user is available"
}
else{
print("user is taken")
userTaken = "user is taken"
}
}
completion(userTaken)
}
This task is a good way to learn some important and helpful things about Swift, such as naming conventions, deciding what to return from functions (you chose a string, I opted for a boolean), accounting for errors, etc.
func checkUsername(_ username: String, completion: #escaping (_ taken: Bool?) -> Void) {
Firestore.firestore().collection("users").whereField("username", isEqualTo: username).getDocuments() { (snapshot, err) in
if let snapshot = snapshot {
if snapshot.documents.isEmpty {
completion(false)
} else {
completion(true)
}
} else {
if let err = err {
print(err)
}
completion(nil)
}
}
}
Usage
checkUsername("drake") { (taken) in
guard let taken = taken else {
// handle error, maybe retry?
return
}
if taken {
// prompt user username is taken
} else {
// username not taken, proceed
}
}
In the signature of the function, I labeled the boolean in the completion closure (taken), which the Swift compiler does not require you do but I think can be very helpful.
By the way, this function can return anything, even a Result object which is a neat way to return an object or an Error in a single object. But I think returning a boolean is straightforward enough here. I made the boolean an optional so the function can return three possible states (true, false, or nil) to give you a way to handle errors.
String return
func checkUsername(_ username: String, completion: #escaping (_ name: String?) -> Void) {
Firestore.firestore().collection("users").whereField("username", isEqualTo: username).getDocuments() { (snapshot, err) in
if let snapshot = snapshot {
if snapshot.documents.isEmpty {
completion(username) // return the username if it's available
} else {
completion("") // return an empty string if taken
}
} else {
if let err = err {
print(err)
}
completion(nil) // return nil if error
}
}
}
checkUsername("drake") { (name) in
guard let name = name else {
// handle error
return
}
if name.isEmpty {
// username taken
} else {
print(name) // username not taken
}
}
I have series of sequential cloud kit calls to fetch records each based on previous fetch. Any one of these fetches may fail and then I have to popover to previous controller. Since there are so many places the fetches can fail, I have to embed popViewController to previous controller in many locations. Can I avoid this and call popover only once if it is possible?
func iCloudSaveMeterHubPrivateDbCz() {
self.clOps.iCloudFetchRecord(recordName: locId, databaseScope: CKDatabaseScope.private, customZone: true, completion: { (locationRecord, error) in
guard error == nil else {
self.navigationController!.popViewController(animated: true)
return
}
self.iCloudFetchMeter(withLocationCKRecord: locationRecord!) { records, error in
if (error != nil ) {
if let ckerror = error as? CKError {
self.aErrorHandler.handleCkError(ckerror: ckerror)
}
self.navigationController!.popViewController(animated: true)
}
if let _ = records?.first {
self.clOps.iCloudFetchRecord(recordName: contactId, databaseScope: CKDatabaseScope.private, customZone: true, completion: { (contactRecord, error) in
if let ckerror = error as? CKError {
self.aErrorHandler.handleCkError(ckerror: ckerror)
self.navigationController!.popViewController(animated: true)
}
DispatchQueue.main.async {
if let record = contactRecord {
record.setObject("true" as NSString, forKey:"assignedEEP")
}
}
}
self.navigationController!.popViewController(animated: true)
}
}
})
}
Whenever I get into nested callbacks like this, I try to consolidate handling a response to a single point in the code. The popular motto of coders helps in this case: "don't repeat yourself"
Here's a suggestion for consolidating error handling and popping to a single place by making your main function have a closure:
func iCloudSaveMeter(finished: #escaping (_ success: Bool, _ error: CKError?) -> Void){
self.clOps.iCloudFetchRecord(recordName: locId, databaseScope: CKDatabaseScope.private, customZone: true, completion: { (locationRecord, error) in
if error != nil {
//:::
finished(false, error)
}
self.iCloudFetchMeter(withLocationCKRecord: locationRecord!) { records, error in
if error != nil {
//:::
finished(false, error)
}
if let _ = records?.first {
self.clOps.iCloudFetchRecord(recordName: contactId, databaseScope: CKDatabaseScope.private, customZone: true, completion: { contactRecord, error in
//:::
finished(false, error)
DispatchQueue.main.async {
if let record = contactRecord {
record.setObject("true" as NSString, forKey:"assignedEEP")
}
}
}
}
//:::
finished(true, nil)
}
})
}
//----
//Call it like this
iCloudSaveMeter(){ success, error in
//Pop
if !success{
self.navigationController!.popViewController(animated: true)
}
//Hande error
if let error = error{
self.aErrorHandler.handleCkError(ckerror: error)
}
}
I have an odd issue and I’m not sure what I am doing wrong.
I have the following function that I want called in viewDidLoad to load all documents in a collection from Firestore that will be displayed in a tableview.
func functionName() -> [String] {
var NameArray = [String]()
db.collection("Names").getDocuments() { (querySnapshot, err) in
if let err = err {
print("Error getting documents: \(err)")
} else {
for document in querySnapshot!.documents {
NameArray.append("\(document.documentID)")
}
}
}
print(NameArray)
print(NameArray.count)
return (NameArray)
}
The function throws a warning result is being ignored. I don’t want to silence it as I need the value, it should return an array with the document names.
When I tried the below code, it returns the array and count as expected.
#IBAction func fetchUsersButton(_ sender: UIButton) {
var NameArray = [String]()
db.collection("Names").getDocuments() { (querySnapshot, err) in
if let err = err {
print("Error getting documents: \(err)")
} else {
for document in querySnapshot!.documents {
NameArray.append("\(document.documentID)")
}
}
print(NameArray)
print(NameArray.count)
}
}
However, I need to be able to return the array created so it can be used outside the function. Is anyone able to help?
Instead of returning an array you need to place it in a completion handler.
func functionName(_ completion: #escaping ([String]) -> Void) {
var NameArray = [String]()
db.collection("Names").getDocuments() { (querySnapshot, err) in
if let err = err {
print("Error getting documents: \(err)")
} else {
for document in querySnapshot!.documents {
NameArray.append("\(document.documentID)")
}
}
completion(NameArray)
}
}
The reason that you aren't returning anything is because db.collection().getDocuments is an asynchronous function. What this means is that the function gets to "return" before the db.collection().getDocuments code is done executing.
"Return" assumes that the code will execute synchronously which means it will execute line by line in a predictable order. An asynchronous function is one in which we don't know when it will finish executing (which is always true of network code).
What if the network is down? What if it takes a long time to download? Since we can't know, we use a completion handler to "return" what we need once the function has completed. The other suggestions are great, below is another solution. As a note, it assumes that this function is part of class, and you want to assign the result to an outside variable.
class MyClass {
var myNameArray = [String]()
func functionName() {
db.collection("Names").getDocuments() { (querySnapshot, err) in
if let err = err {
print("Error getting documents: \(err)")
} else {
for document in querySnapshot!.documents {
myNameArray.append("\(document.documentID)")
}
}
}
}
}
Another small thing about naming conventions, variables should utilize camelCase, so nameArray is preferable to NameArray.