I'm trying to read data from Firebase, and write it in a tableView but the data is not populating the tableView
When I print the data inside the closure where I read the data, it prints correctly, but outside the closure it prints blank values. It also prints correctly inside viewDidAppear
import UIKit
import Firebase
class UserProfileTableViewController: UIViewController, UITabBarDelegate, UITableViewDataSource {
private var gotName: String = ""
private var gotAdress: String = ""
private var gotPhone: String = ""
#IBOutlet var tableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
self.tableView.separatorColor = UIColor.gray
//Get userinfo from database
let uid = Auth.auth().currentUser!.uid
let userInfoRef = Database.database().reference().child("userprofiles/\(uid)")
userInfoRef.observeSingleEvent(of: .value, with: { (snapshot) in
// Get user value
let value = snapshot.value as? NSDictionary
let name = value?["Name"] as? String ?? ""
let address = value?["Address"] as? String ?? ""
let phone = value?["Phone"] as? String ?? ""
self.gotName = name
self.gotAdress = address
self.gotPhone = phone
print("Print inside closure in viewDidLoad\(self.gotName, self.gotAdress, self.gotPhone)") //This prints the correct data
// ...
}) { (error) in
print(error.localizedDescription)
}
let testRef = Database.database().reference().child("Test")
testRef.setValue(gotName) // Sets value to ""
print("Print inside outside in viewDidLoad\(self.gotName, self.gotAdress, self.gotPhone)") //This prints blank values
}
override func viewDidAppear(_ animated: Bool) {
print("Print in viewDidAppear closure\(self.gotName, self.gotAdress, self.gotPhone)") //This prints the correct data
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 1
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "UserProfileCell") as! UserProfileCell
cell.userProfileLabel.text = gotName
return cell
}
The print statement outside the closure where I read data in viewDidLoad is the first to be printed in the console if that matters?
Getting data from Firebase or from any server service is done in asynchronous way. That's why when you try to print variables outside closures it doesn't print anything. Try calling tableView.reloadData() inside closure and it will show your desired data.
Related
The goal here is to get specific information from Firebase Firestore when tapping on a specific row from Table View in Xcode. I have already found a way to store the data into a Table View:
func loadData() {
FirebaseFirestore.root.collection("users").getDocuments { (snapshot, error) in
if let error = error {
print(error.localizedDescription)
} else {
if let snapshot = snapshot {
for document in snapshot.documents {
let data = document.data()
let name = data["name"] as? String ?? ""
let email = data["email"] as? String ?? ""
let phone = data["phone"] as? String ?? ""
let newPerson = Person(name: name, email: email, phone: phone)
self.people.append(newPerson)
print (self.people)
let newIndexPath = IndexPath(row: self.people.count - 1, section: 0)
self.table.beginUpdates()
self.table.insertRows(at: [newIndexPath], with: .automatic)
self.table.endUpdates()
}
}
}
}
}
And when running, this shows up:
When I tap row 1, for example, I want to segue into another view that displays the name, email, phone for each specific row. Is this possible? And if so, how would I do this?
------------------------------
So far, I thought of adding another variable to each row, but not displaying on the row. Since in Firebase, the document id is the email, I can set the var email to every row, and use getDocument() to retrieve the document by email, the document id.
All I've done was use the variable email to try to use getDocument on the didSelectRowAt function.
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let tappedUser = FirebaseFirestore.root.collection("users").document(email)
performSegue(withIdentifier: "toPersonDesc", sender: nil)
}
But this doesn't work. I don't think I can use the email var since it gives an error for Person.email, and just email.
Any help would be appreciated.
-------------------------------
Entire code block if anyone needs it (updated to what I've tried):
import UIKit
import FirebaseFirestore
struct Person {
let name, phone, email: String
}
class Cell: UITableViewCell {
#IBOutlet weak var phone: UILabel!
#IBOutlet weak var name: UILabel!
#IBOutlet weak var email: UILabel!
}
class PeopleViewController: UITableViewController {
#IBOutlet var table: UITableView!
var people = [Person]()
private var document: [DocumentSnapshot] = []
override func viewDidLoad() {
super.viewDidLoad()
table.tableFooterView = UIView(frame: CGRect.zero)
self.table.delegate = self
self.table.dataSource = self
loadData()
}
// MARK: - Table view data source
override func numberOfSections(in tableView: UITableView) -> Int {
// #warning Incomplete implementation, return the number of sections
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// #warning Incomplete implementation, return the number of rows
return self.people.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let person = people[indexPath.row]
let cell = self.table.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! Cell
cell.name.text = person.name
cell.phone.text = person.phone
cell.email.text = person.email
return cell
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let tappedUser = FirebaseFirestore.root.collection("users").document(Person.email)//Error here
performSegue(withIdentifier: "toPersonDesc", sender: nil)
}
func loadData() {
FirebaseFirestore.root.collection("users").getDocuments { (snapshot, error) in
if let error = error {
print(error.localizedDescription)
} else {
if let snapshot = snapshot {
for document in snapshot.documents {
let data = document.data()
let name = data["name"] as? String ?? ""
let phone = data["phone"] as? String ?? ""
let email = data["email"] as? String ?? ""
let newPerson = Person(name: name, phone: phone, email: email)
self.people.append(newPerson)
print (self.people)
let newIndexPath = IndexPath(row: self.people.count - 1, section: 0)
self.table.beginUpdates()
self.table.insertRows(at: [newIndexPath], with: .automatic)
self.table.endUpdates()
}
}
}
}
}
Hey this actually isn't that difficult fortunately. All you need to do is something like this:
//Create a custom UIViewController class which has a property of type Person.
class CustomViewController : UIViewController {
var person: Person?
// You'll need to set this up using a .xib file
weak var customView:CustomView?
}
class UsersViewController : UIViewController, UITableViewDelegate {
var person:[Person]?
//This method will be executed every time the user clicks on the row. Make sure that your current class adopts the UITableViewDelegate protocol.
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
//Insert your functionality to get the correct person and set the person property in the new custom view controller.
let viewController = CustomViewController()
viewController.person = self.person?[indexPath.row]
// Display the new custom view controller.
self.navigationController?.pushViewController(viewController, animated: true)
}
}
Explanation:
Create a custom UIViewController class which has a property of type Person.
This method - func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) - will be executed every time the user clicks on the row. Make sure that your current class adopts the UITableViewDelegate protocol.
Within the didSelectRow function, insert your functionality to get the correct person and set the person property in the new custom view controller.
Display the new custom view controller.
You will need to make sure that you set up the view of the view controller to display the data in the person object. Check out this video if you need to know how to do that.
If you want any clarification of this just let me know!
Edit:
There are multiple ways to do this. Firebase has a functionality to watch a document and if there are changes to the document server-side than you can apply those changes in the app. Check it out here.
I would probably look into using something like that.
Or you can fetch the data within the didSelectRow and than once the data is retrieved show the new view controller.
I am trying to show User ID's in my app, however the UID's are very random and next to each one is a name, is it possible to show both the name of the nodes as well as its value?
My firebase database
I want to display the Name "Andy" as well as the UID "uLUnOelxABYl3lCtLz2Of5Yfnvc2"
I have set it up so that the App receives the values from my Firebase.
Here is my code:
class TestViewController: UIViewController, UITableViewDelegate, UITableViewDataSource{
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return list.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell")
cell?.textLabel?.text = list[indexPath.row]
return cell!
}
#IBOutlet weak var tableView: UITableView!
var ref: DatabaseReference!
var list = [String] ()
override func viewDidLoad() {
super.viewDidLoad()
tableView.delegate = self
tableView.dataSource = self
if FirebaseApp.app() == nil {
FirebaseApp.configure() }
ref = Database.database().reference()
ref?.child("users").observe(.childAdded, with: { (snapshot) in
let list1 = snapshot.value as? String
if let actualList = list1 {
self.list.append(actualList)
self.tableView.reloadData()
}
}
)
}
}
Here is what I get back. However I want to show is a list of the names with these values next to it, like this (photoshop)
results
PHOTOSHOP RESUTLS
Regarding to documentation FIRDataSnapshot has property key, so I think you can use it for matching with values, e.g:
/// e.g. use dictionary instead of Array
//var list = [String] ()
var list = [AnyHashable: String]()
...
ref?.child("users").observe(.childAdded, with: { (snapshot) in
let key = snapshot.key as? Hashable
let value = snapshot.value as? String
if let actualValue = value, let actualKey = key {
self.list[key] = actualValue
self.tableView.reloadData()
}
}
I'm trying to retrieve the child in Firebase and print them EACH in a label but when I'm retrieving the key its printing all at the same time. I'm still learning on the go so here's what i tried:
func retrieve(){
let userId = Auth.auth().currentUser?.uid
let intakeRef = Database.database().reference().child("Users").child(userId!).child("Intake")
intakeRef.observeSingleEvent(of: .value, with: { (snapshot) in
if let intake = snapshot.value{
self.ref.child("Attendance").child("Checkin").child(intake as! String).child(userId!).child("BM050-3-3-IMNPD").observe(.value, with: { (snapshot) in
for child in snapshot.children{
let key = (child as AnyObject).key as String
print(key)
label.text = key
}
})
}
}) { (Error) in
print("unable to retrieve name from nameRef")
}
}
The code I tried like i said is printing all at the same time. But i would like to print each one in a separate label. Any help would really be appreciated!
You'll of course need to create a tableViewController and link it up to a TableView and assign your cell its own identifier, but this is a good outline of what you'd need to do in your own tableViewController:
class tableViewController: UITableViewController {
var dataArray = [String]()
var ref = Database.database().reference() //You didn't include your delcaration of ref in your code, so don't use this, just use the one you were already using.
override func viewDidLoad() {
super.viewDidLoad()
retrieve()
}
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return dataArray.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell")
let data = dataArray[indexPath.row]
cell.textLabel?.text = data
return cell
}
func retrieve(){
let userId = Auth.auth().currentUser?.uid
let intakeRef = Database.database().reference().child("Users").child(userId!).child("Intake")
intakeRef.observeSingleEvent(of: .value, with: { (snapshot) in
if let intake = snapshot.value{
self.ref.child("Attendance").child("Checkin").child(intake as! String).child(userId!).child("BM050-3-3-IMNPD").observe(.value, with: { (snapshot) in
for child in snapshot.children{
let key = (child as AnyObject).key as String
self.dataArray.append(key)
}
})
self.tableView.reloadData()
}
}) { (Error) in
print("unable to retrieve name from nameRef")
}
}
}
Again you'll need to create your own UItableView, link it up to a UITableViewController, assign your own cell reuse identifier and make sure that cellForRowAt has the correct reuse identifier in it, and you'll need to change the ref variable in my provided code to the ref that you need to use.
I'm trying to use a JSON file to populate a UITableView in my app. Previously I was hard coding an array of sample data, but need to move to using my JSON file. This is a mishmash of various tutorials and answers found on SO, so I apologize if the syntax conventions are a little off.
import UIKit
import os.log
class BonusTableViewController: UITableViewController {
//MARK: Properties
var bonuses = [Bonus]() // Used for old sample data
var jBonuses = [Bonuses]() // Used with JSON based data
override func viewDidLoad() {
super.viewDidLoad()
//MARK: Confirm JSON file was loaded and log the Bonus Codes
let loadedBonuses = loadJson(filename: "BonusData")
for bonus in loadedBonuses! {
print(bonus.bonusCode)
}
}
// Load the JSON file from the bundled file.
func loadJson(filename fileName: String) -> [Bonuses]? {
if let url = Bundle.main.url(forResource: fileName, withExtension: "json") {
do {
let data = try Data(contentsOf: url)
let decoder = JSONDecoder()
let jsonData = try decoder.decode(JSONData.self, from: data)
print("loadJson loaded JSON")
return jsonData.bonuses
} catch {
print("error:\(error)")
}
}
return nil
}
// MARK: Data Structures
// Bonus Data Structs
struct JSONData: Decodable {
let name: String
let version: String
let bonuses: [Bonuses]
}
struct Bonuses: Decodable {
let bonusCode: String
let category: String
let name: String
let value: Int
let city: String
let state: String
let flavor: String
let imageData: String
}
// MARK: - Table view data source
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return jBonuses.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// Table view cells are reused and should be dequeued using a cell identifier.
let cellIdentifier = "BonusTableViewCell"
guard let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath) as? BonusTableViewCell else {
fatalError("The dequeued cell is not an instance of BonusTableViewCell.")
}
// Now using JSON file
let jBonus = jBonuses[indexPath.row]
print("Setting labels using JSON file")
cell.bonusCodeLabel.text = jBonus.bonusCode
cell.categoryLabel.text = jBonus.category
cell.nameLabel.text = jBonus.name
cell.valueLabel.text = "\(jBonus.value)"
cell.cityLabel.text = "\(jBonus.city),"
cell.stateLabel.text = jBonus.state
cell.flavorText.text = jBonus.flavor
cell.primaryImage.image = jBonus.photo
return cell
}
From the console, I can confirm that it is able to see the JSON data and it does spit out the list of bonus codes. I can't pinpoint why this isn't working, but the result is a blank just a tableview with a bunch of empty rows.
Replace viewDidLoad with
override func viewDidLoad() {
super.viewDidLoad()
jBonuses = loadJson(filename: "BonusData")!
tableView.reloadData()
}
You have to assign the loaded data to the data source array and reload the table view.
Or if loadedBonuses could really be nil (it cannot in this case):
override func viewDidLoad() {
super.viewDidLoad()
if let loadedBonuses = loadJson(filename: "BonusData") {
jBonuses = loadedBonuses
tableView.reloadData()
}
}
Notes:
Delete the method numberOfSections, 1 is the default.
Force unwrap the cell, the code must not crash if everything is hooked up properly
let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath) as! BonusTableViewCell
You are populating Tableview using jBonuses array of Type Bonuses, but where you are filling the jBonuses array.
It seems that you are not filling jBonuses Array. Fill the jBonuses array once you get API response and call tableview reloadData method.
yourTableView.reloadData()
It's been now several hours I'm trying to dig Internet forums to try to find a solution to my problem and I can't figure out what's happening so I hope someone here can help)
I have a swift app and on one of my viewcontrollers, what I would want to do is to retrieve data from Firebase database and display them on my tableview.
Here you can see how my data are organized. Firebase org of data
To get the number of rows in my tableview, I put this into the viewWillAppear section
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
// Count the number contacts to get the correct number of lines in the table
let ref_table = ref.child("users").child((user?.uid)!).child("emergencyContacts")
print("Starting observing")
ref_table.observe(.value, with: { (snapshot: DataSnapshot!) in
print("Got snapshot")
print(snapshot.childrenCount)
let count = snapshot.childrenCount
self.numberOfContact = Int(count)
self.UI_contactsTableView.reloadData()
ref_table.removeAllObservers()
})
print("Returning count")
}
Then, to get the number of lines, I have this
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
print("Number of contact TV: \(numberOfContact)")
return numberOfContact
}
And to fill the cells of my tableview
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "contact-cell") as! EmergencyContactCell
print("value of numberOfContact \(numberOfContact)")
ref.child("users").child((user?.uid)!).child("emergencyContacts").observeSingleEvent(of: .value, with: { (snapshot: DataSnapshot) in
print("snapshot value \(snapshot.value!)")
for rest in snapshot.children.allObjects as! [DataSnapshot] {
guard let restDict = rest.value as? [String:Any] else { continue }
let firstName = restDict["contact_firstName"] as? String ?? ""
cell.UI_fullName.text = "\(firstName)"
}
})
return cell
}
And I think this is where it gets bad because in the console, whatever I do, I constantly have 2 sets of data displayed so my cells are always filled with the same name as you can see here.
This is also a capture of my console after program execution.
Starting observing
Returning count
Number of contact TV: 0
Number of contact TV: 0
Number of contact TV: 0
Got snapshot
2
Number of contact TV: 2
value of numberOfContact 2
value of numberOfContact 2
snapshot value {
1ArkUu6pMPZPgf3pGFAvvxHiTPFaf5Cl = {
"contact_firstName" = Kate;
"contact_lastName" = Bell;
"contact_phoneNumber" = "(555) 564-8583";
};
s8haDrYYAT9Y12ZnmAfE87pDyjDZGwjx = {
"contact_firstName" = Daniel;
"contact_lastName" = Higgins;
"contact_phoneNumber" = "555-478-7672";
};
}
snapshot value {
1ArkUu6pMPZPgf3pGFAvvxHiTPFaf5Cl = {
"contact_firstName" = Kate;
"contact_lastName" = Bell;
"contact_phoneNumber" = "(555) 564-8583";
};
s8haDrYYAT9Y12ZnmAfE87pDyjDZGwjx = {
"contact_firstName" = Daniel;
"contact_lastName" = Higgins;
"contact_phoneNumber" = "555-478-7672";
};
}
Thanks in advance for your help:)
There is a an error in your cellForRowAtIndex Path method.
`ref.child("users").child((user?.uid)!).child("emergencyContacts").observeSingleEvent(of: .value, with: { (snapshot: DataSnapshot) in'
The above line returns all the emergency contacts each time the cellForRowAtIndexPath method is called.
In your for-loop you then always loop through and pick the last value.
You would need to extract the value for the particular index path.
I figured it out, I tried to have a total different reasoning and it worked. I actually used a handler to manage all this. I post my code here, it could help people in the same trouble as I was couple of hours ago.
var emergencyContactList = [EmergencyContact]()
override func viewDidLoad() {
super.viewDidLoad()
ref = Database.database().reference()
fetchUsers()
}
func fetchUsers() {
refHandle = ref.child("users").child((user?.uid)!).child("emergencyContacts").observe(.childAdded, with: { (snapshot) in
if let dictionary = snapshot.value as? [String:AnyObject] {
print(dictionary)
let emergencyContact = EmergencyContact()
emergencyContact.setValuesForKeys(dictionary)
self.emergencyContactList.append(emergencyContact)
DispatchQueue.main.async {
self.UI_contactsTableView.reloadData()
}
}
})
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return emergencyContactList.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "contact-cell") as! EmergencyContactCell
let _firstName = emergencyContactList[indexPath.row].contact_firstName ?? ""
let _lastName = emergencyContactList[indexPath.row].contact_lastName ?? ""
let _phoneNumber = emergencyContactList[indexPath.row].contact_phoneNumber ?? ""
cell.UI_fullName.text = "\(_firstName) \(_lastName)"
cell.UI_phoneNumber.text = "\(_phoneNumber)"
return cell
}
And I have a swift file apart EmergencyContact.swift
import Foundation
class EmergencyContact: NSObject {
#objc var contact_firstName:String?
#objc var contact_lastName:String?
#objc var contact_phoneNumber:String?
}
Now it works like a charm!
Thanks TheAppMentor for having taken the time to answer me :)