Having trouble populating tableView using Firebase/Firestore in iOS - ios

This is my first time working with a cloud database and I'm looking for a little bit of guidance here as I'm relatively new to programming, and Firestore in particular.
I'm trying to get() all of my document data inside of my viewDidLoad and store it inside of a dictionary so that I can use it later in the tableView dataSource methods to populate my tableView Sections and Rows.
I'm working on a gym/workout log app and I inputted some dummy data for Days and Workouts Collections, so my dictionary prints out like this...
dataDict = ["Monday": ["Chest", "Arms"], "Wednsday": ["Legs", "Arms"], "Tuesday": ["Back"]]
But I'm having trouble using this data to populate the fields because if I try to print out the results of dataDict outside of the dateWorkoutRequest function, like inside of my dataSource methods, I get an empty dictionary. Is my tableView.reloadData() in the wrong place? Should I be using a dictionary to parse my data or is that a bad idea?
Here is my data structure and the relevant code...
/users/mi9P3TrLwkQejYo3oDIu/Days/WZ3Q6LDuu1kja5Rc/Workouts/BpLGFREoJNzNQW
var daysArray = [String]()
var dayIdArray = [String]()
var dataDict : [String:[String]] = [:]
//MARK: - viewDidLoad()
override func viewDidLoad() {
super.viewDidLoad()
vcBackgroundImg()
navConAcc()
picker.delegate = self
picker.dataSource = self
tableView.register(UITableViewCell.self, forCellReuseIdentifier: cellID)
tableView.tableFooterView = UIView()
Auth.auth().addStateDidChangeListener { (auth, user) in
self.userIdRef = user!.uid
self.colRef = Firestore.firestore().collection("/users/\(self.userIdRef)/Days")
self.dateWorkoutRequest()
}
}
func dateWorkoutRequest(){
self.colRef.getDocuments { (snapshot, err) in
if let err = err
{
print("Error getting documents: \(err)");
}
else {
//Appending all Days collection documents with a field of "dow" to daysarray...
for dayDocument in snapshot!.documents {
self.daysArray.append(dayDocument.data()["dow"] as? String ?? "")
self.dayIdArray.append(dayDocument.documentID)
Firestore.firestore().collection("/users/\(self.userIdRef)/Days/\(dayDocument.documentID)/Workouts/").getDocuments { (snapshot, err) in
if let err = err
{
print("Error getting documents: \(err)");
}
else {
//Assigning all Workouts collection documents belonging to selected \(dayDocument.documentID) to dictionary dataDict...
for document in snapshot!.documents {
if self.dataDict[dayDocument.data()["dow"] as? String ?? ""] == nil {
self.dataDict[dayDocument.data()["dow"] as? String ?? ""] = [document.data()["workout"] as? String ?? ""]
} else {
self.dataDict[dayDocument.data()["dow"] as? String ?? ""]?.append(document.data()["workout"] as? String ?? "")
}
print(self.dataDict)
}
}
}
}
self.dayCount = snapshot?.count ?? 0
self.tableView.reloadData()
}
}
}

I would try to put the self.tableView.reloadData() just after the line that you append document in the array.
I think that it's interesting you put DispatchQueue.main.async block in the reloadData too.
Like this:
func dateWorkoutRequest(){
self.colRef.getDocuments { (snapshot, err) in
if let err = err
{
print("Error getting documents: \(err)");
}
else {
//Appending all Days collection documents with a field of "dow" to daysarray...
for dayDocument in snapshot!.documents {
self.daysArray.append(dayDocument.data()["dow"] as? String ?? "")
self.dayIdArray.append(dayDocument.documentID)
Firestore.firestore().collection("/users/\(self.userIdRef)/Days/\(dayDocument.documentID)/Workouts/").getDocuments { (snapshot, err) in
if let err = err
{
print("Error getting documents: \(err)");
}
else {
//Assigning all Workouts collection documents belonging to selected \(dayDocument.documentID) to dictionary dataDict...
for document in snapshot!.documents {
if self.dataDict[dayDocument.data()["dow"] as? String ?? ""] == nil {
self.dataDict[dayDocument.data()["dow"] as? String ?? ""] = [document.data()["workout"] as? String ?? ""]
} else {
self.dataDict[dayDocument.data()["dow"] as? String ?? ""]?.append(document.data()["workout"] as? String ?? "")
}
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
}
}
}
self.dayCount = snapshot?.count ?? 0
}
}
}

Related

Swift Firestore - TableView Button Bug

Please look at my TableView with custom Cell here. I fetched the data from Firestore (and sort it by date descendingly) real time and insert it into the meal array locally. When I clicked the 'eat' button and add a new meal, the 'eat' button isn't sorted correctly?
Here is my code for loadMenu() function and eatButtonPressed() function
loadMenu()
func loadMenu() {
let menuRef = db.collection("menu").document()
db.collection("menu").order(by: "date", descending: true).whereField("family_id", isEqualTo: "\(UserDefaults.standard.string(forKey: "family_id")!)")
.addSnapshotListener { querySnapshot, error in
self.menu = []
guard let documents = querySnapshot?.documents else {
print("Error fetching documents: \(error!)")
return
}
let name = documents.map { $0["name"] ?? [""] }
let family_id = documents.map { $0["family_id"] ?? [0] }
let portions = documents.map { $0["portions"] ?? [""] }
let menu_id = documents.map { $0["menu_id"] ?? [""]}
let isOpened = documents.map { $0["isOpened"] ?? [""]}
if name != nil || name[0] as! String != "" {
for i in 0..<name.count {
self.menu.append(Menu(menu_id: menu_id[i] as! String, name: name[i] as! String, family_id: family_id[i] as! String, portions: portions[i] as! Int, isOpened: isOpened[i] as! Bool))
}
}
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
}
eatButtonPressed()
func eatButtonPressed(cell: TopPartTableViewCell, send: UIButton) {
var likeRef = self.db.collection("like").document("\(Auth.auth().currentUser!.uid)_\(self.menu[send.tag].menu_id)")
self.db.collection("dislike").document("\(Auth.auth().currentUser!.uid)_\(self.menu[send.tag].menu_id)").delete() { err in
if let err = err {
print("Error removing document: \(err)")
} else {
print("Document successfully removed!")
}
}
likeRef.getDocument { (document, error) in
if let document = document, document.exists {
let dataDescription = document.data().map(String.init(describing:)) ?? "nil"
self.db.collection("like").document("\(Auth.auth().currentUser!.uid)_\(self.menu[send.tag].menu_id)").delete() { err in
if let err = err {
print("Error removing document: \(err)")
} else {
print("Document successfully removed!")
}
}
} else {
likeRef.setData([
"like_id": "\(likeRef.documentID)",
"user_id": "\(Auth.auth().currentUser!.uid)",
"menu_id": "\(self.menu[send.tag].menu_id)"
]) { err in
if let err = err {
print("Error writing document: \(err)")
} else {
print("Document successfully written!")
}
}
}
}
let user_liked = like.contains(where: {$0.menu_id == menu[send.tag].menu_id}) && like.contains(where: {$0.user_id == Auth.auth().currentUser!.uid})
if !user_liked {
send.backgroundColor = UIColor(named: "BrandOrange")
send.tintColor = UIColor.white
cell.dontEatButton.backgroundColor = UIColor.white
cell.dontEatButton.tintColor = UIColor.black
} else {
send.backgroundColor = UIColor.white
send.tintColor = UIColor.black
}
}
Anyone can help me solve this problem?
Thank you.
The above method doesn't seem to be relevant to the question.
You have added a new item and a cell has been added for that item.
If you look at the detailed structure of the cell, I think you can figure out the cause.
If the cell default button state is Eat, it is most likely not initialized properly.

reading data from firestore and save locally in an array

I want to retrieve usernames from a users collection and save in an array. I use this:
var usernames:[String] = []
override func viewDidLoad() {
super.viewDidLoad(
populateUsernames()
}
func populateUsernames() {
let db = Firestore.firestore()
db.collection("users").getDocuments() { [self] (querySnapshot, err) in
if let err = err {
print("Error getting documents: \(err)")
} else {
for document in querySnapshot!.documents {
let username = document.get("username") as! String
usernames.append(username)
print(usernames) //THIS PRINTS ["user1", "user2"] WHICH IS CORRECT
}
print(usernames) // THIS PRINTS [] WHICH IS FALSE
}
}
}
Why does the array reset to [] after the for loop?
There is nothing in your code that would cause this behavior. You're either printing the wrong array or something else is overwriting it, which doesn't seem likely. I notice that you aren't referring to the array with self which you would need to do in this closure. Therefore, rename the array for testing purposes.
var usernames2: [String] = []
override func viewDidLoad() {
super.viewDidLoad()
populateUsernames()
}
func populateUsernames() {
Firestore.firestore().collection("users").getDocuments { (snapshot, error) in
if let snapshot = snapshot {
for doc in snapshot.documents {
if let username = doc.get("username") as? String {
self.usernames2.append(username)
print(username)
} else {
print("username not found")
}
}
print(self.usernames2)
} else {
if let error = error {
print(error)
}
}
}
}
You also crudely parse these documents which may not be harmful but is nonetheless unsafe, which this code addresses.

Retrieve Firestore collection data - Error -Unexpectedly found nil while implicitly unwrapping an Optional value

I have been working on Firestore for retrieving data, when I tried to get data from collection->document id-> field. refer the below screen shot, I need to check companyCode matches with user entered companyCode.text
I tried with below code, need to check whether the user entered companyCodeLabel.text matches document "companyCode" and also get documentId. Can anyone suggest how to solve this?
guard let code = companyCodeLabel.text else { return }
let docRef = db.collection("Company").whereField("companyCode", isEqualTo: code).limit(to: 1)
docRef.getDocuments { (querysnapshot, error) in
if error != nil {
print("Document Error: ", error!)
} else {
if let doc = querysnapshot?.documents, !doc.isEmpty {
print("Document is present.")
}
}
}
Even tried to print the field value in collection but still have crash and same error nil
self.db.collection("Company").getDocuments { (snapshot, err) in
if let err = err {
print("Error getting documents: \(err)")
} else {
for document in snapshot!.documents {
let docId = document.documentID
let compCode = document.get("companyCode") as! String
let compName = document.get("companyName") as! String
print(docId, compCode, compName)
}
}
}
I tried to call in wrong db, I was trying var db = Firestore!,
The correct solutions is
Firestore.firestore().collection("Company").getDocuments { (snapshot, err) in
if let err = err {
print("Error getting documents: \(err)")
} else {
for document in snapshot!.documents {
let docId = document.documentID
let compCode = document.get("companyCode") as! String
let compName = document.get("companyName") as! String
print(docId, compCode, compName)
}
}

AddSnapShotListener is repeating the document reading in one instance

When I am using addSnapshotListener for realtime updates, the documents are repeated which should not be the case, but when using getDocuments() the documents are repeated once only, I need to use addSnaphotListener but not want to duplicate the document reading, please assist where I am wrong in using snapshot listener.
I am using Firestore database in Swift iOS. Below is the code I am using
Code with addSnapShotListener():
func getComments() {
//print(postId + "received")
let commentsRef = Firestore.firestore().collection("posts").document(postId).collection("comments")
commentsRef.addSnapshotListener { (snapshot, error) in
if let error = error {
print(error.localizedDescription)
} else {
if let snapshot = snapshot {
for document in snapshot.documents {
// self.length = snapshot.count
let data = document.data()
let username = data["comment_author_username"] as? String ?? ""
let comment = data["comment_author_comment"] as? String ?? ""
let spinnerC = data["comment_author_spinnerC"] as? String ?? ""
let fullname = data["comment_author_fullname"] as? String ?? ""
let email = data["comment_author_email"] as? String ?? ""
let commentUserImageUrl = data["comment_user_image"] as? String ?? ""
let commentuser_id = data["comment_author_id"] as? String ?? ""
self.checkl1value = data["l1"] as? Bool
let newComment = Comment(_documentId: document.documentID, _commentAuthorUsername: username, _commentAuthorFullName: fullname, _commentAuthorComment: comment, _commentUserImage: commentUserImageUrl, _commentAuthorSpinnerC: spinnerC, _commentAuthorId:commentuser_id, _checkl1value: self.checkl1value)
self.comments.append(newComment)
// print(self.length!)
}
self.tableView.reloadData()
}
}
}
}
Code With getDocuments():
func getComments() {
//print(postId + "received")
let commentsRef = Firestore.firestore().collection("posts").document(postId).collection("comments")
commentsRef.getDocuments { (snapshot, error) in
if let error = error {
print(error.localizedDescription)
} else {
if let snapshot = snapshot {
for document in snapshot.documents {
// self.length = snapshot.count
let data = document.data()
let username = data["comment_author_username"] as? String ?? ""
let comment = data["comment_author_comment"] as? String ?? ""
let spinnerC = data["comment_author_spinnerC"] as? String ?? ""
let fullname = data["comment_author_fullname"] as? String ?? ""
let email = data["comment_author_email"] as? String ?? ""
let commentUserImageUrl = data["comment_user_image"] as? String ?? ""
let commentuser_id = data["comment_author_id"] as? String ?? ""
self.checkl1value = data["l1"] as? Bool
let newComment = Comment(_documentId: document.documentID, _commentAuthorUsername: username, _commentAuthorFullName: fullname, _commentAuthorComment: comment, _commentUserImage: commentUserImageUrl, _commentAuthorSpinnerC: spinnerC, _commentAuthorId:commentuser_id, _checkl1value: self.checkl1value)
self.comments.append(newComment)
// print(self.length!)
}
self.tableView.reloadData()
}
}
}
}
You're probably looking to only handle the changes between the snapshots. To do that you'll want to loop over instead of, as shown in the documentation on viewing changes between snapshots:
db.collection("cities").whereField("state", isEqualTo: "CA")
.addSnapshotListener { querySnapshot, error in
guard let snapshot = querySnapshot else {
print("Error fetching snapshots: \(error!)")
return
}
snapshot.documentChanges.forEach { diff in
if (diff.type == .added) {
print("New city: \(diff.document.data())")
}
if (diff.type == .modified) {
print("Modified city: \(diff.document.data())")
}
if (diff.type == .removed) {
print("Removed city: \(diff.document.data())")
}
}
}
Initially your listener will get called with diff.type == .added for each existing document, and then when there are changes it'll get called with the right mix of types.

How to create array in array using collections and sub collections in Firestore?

I need to create an array of Categories that contains Questions array.
struct CategoryFB {
var title: String
var id: Int
var questions: [QuestionsFB]
var dictionary: [String : Any] {
return ["title" : title,
"id" : id]
}
}
extension CategoryFB {
init?(dictionary: [String : Any], questions: [QuestionsFB]) {
guard let title = dictionary["title"] as? String, let id = dictionary["id"] as? Int else { return nil }
self.init(title: title, id: id, questions: questions)
}
}
Firestore has a following structure
Collection("Categories")
Document("some_id")
Collection("Questions")
How to create array like this?
array = [Category(title: "First",
questions: [
Question("1"),
...
]),
... ]
My try was wrong:
db.collection("Categories").order(by: "id", descending: false).getDocuments {
(querySnapshot, error) in
if error != nil {
print("Error when getting data \(String(describing: error?.localizedDescription))")
} else {
for document in querySnapshot!.documents {
print(document.documentID)
self.db.collection("Categories").document(document.documentID).collection("Questions").getDocuments(completion: { (subQuerySnapshot, error) in
if error != nil {
print(error!.localizedDescription)
} else {
var questionsArray: [QuestionsFB]?
questionsArray = subQuerySnapshot?.documents.compactMap({QuestionsFB(dictionary: $0.data())})
self.categoriesArray = querySnapshot?.documents.compactMap({CategoryFB(dictionary: $0.data(), questions: questionsArray!)})
print(self.categoriesArray![0].questions.count)
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
})
}
}
}
Your main problem seems to stem from the fact that you're regenerating your categories array every time you run your subquery, and when you do, you're only supplying a single questions array to the entire thing.
There's lots of ways to fix this. I would probably break it up so that you a) First allow yourself to create a category array without any questions, and then b) Go back through each of your individual subQueries and insert them into your categories as you get them.
Your final code might look something like this. Note that this would mean changing your Category object so that you can first create it without a Questions array, and implementing this custom addQuestions:toCategory: method (which would be a whole lot easier if you stored your categories as a dictionary instead of an array)
db.collection("Categories").order(by: "id", descending: false).getDocuments {
(querySnapshot, error) in
if error != nil {
print("Error when getting data \(String(describing: error?.localizedDescription))")
} else {
self.categoriesArray = querySnapshot?.documents.compactMap({CategoryFB(dictionary: $0.data()})
for document in querySnapshot!.documents {
print(document.documentID)
self.db.collection("Categories").document(document.documentID).collection("Questions").getDocuments(completion: { (subQuerySnapshot, error) in
if error != nil {
print(error!.localizedDescription)
} else {
var questionsArray: [QuestionsFB]?
questionsArray = subQuerySnapshot?.documents.compactMap({QuestionsFB(dictionary: $0.data())})
self.addQuestions(questionsArray toCategory: document.documentID)
print(self.categoriesArray![0].questions.count)
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
})
}
}
}
Alternately, if you think you're going to be in a situation where you're always going to want to grab your questions every time you want to grab a category, you might consider not putting them in a subcollection at all, and just making them a map in the original category document.
This is the solution which I found by myself. Hopefully this will help someone in the future.
func getData(completion: #escaping (_ result: [Any]) -> Void) {
let rootCollection = db.collection("Categories")
var data = [Any]()
rootCollection.order(by: "id", descending: false).getDocuments(completion: {
(querySnapshot, error) in
if error != nil {
print("Error when getting data \(String(describing: error?.localizedDescription))")
} else {
guard let topSnapshot = querySnapshot?.documents else { return }
for category in topSnapshot {
rootCollection.document(category.documentID).collection("Questions").getDocuments(completion: {
(snapshot, err) in
guard let snapshot = snapshot?.documents else { return }
var questions = [Question]()
for document in snapshot {
let title = document.data()["title"] as! String
let details = document.data()["details"] as! String
let article = document.data()["article"] as! String
let link = document.data()["link"] as! String
let id = document.data()["id"] as! String
let possibleAnswers = document.data()["possibleAnswers"] as! [String]
let rightAnswerID = document.data()["rightAnswerID"] as! Int
let newQuestion = Question(title: title, article: article, details: details, link: link, possibleAnswers: possibleAnswers, rightAnswerID: rightAnswerID, id: id)
questions.append(newQuestion)
}
let categoryTitle = category.data()["title"] as! String
let collectionID = category.data()["id"] as! Int
let newCategory = Category(title: categoryTitle, id: collectionID, questions: questions)
data.append(newCategory)
//Return data on completion
completion(data)
})
}
}
})
}

Resources