I am a newbie to swift and firebase, I am trying to populate my tabelview with firebase data. When I run the program, nothing shows up in tableview. Any help would be gladly appreciated. This is what I got do far, tried to read the documents, but its not helping.
import UIKit
import Firebase
import FirebaseUI
class ChurchTableViewController: UITableViewController {
let firebase = Firebase(url:"https://.....com/")
var items = [NSDictionary]()
override func viewDidLoad() {
super.viewDidLoad()
// Uncomment the following line to preserve selection between presentations
// self.clearsSelectionOnViewWillAppear = false
// Uncomment the following line to display an Edit button in the navigation bar for this view controller.
//self.navigationItem.rightBarButtonItem = self.editButtonItem()
}
override func viewDidAppear(animated: Bool) {
//MARK: Load data from firebsr
firebase.observeEventType(.Value, withBlock: { snapshot in
print(snapshot.value)
}, withCancelBlock: { error in
print(error.description)
})
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
// MARK: - Table view data source
override func numberOfSectionsInTableView(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 items.count
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath)
let dict = items[indexPath.row]
cell.textLabel?.text = dict["ChurchName"] as? String
return cell
}
You've created the observer for when some value changes in your Firebase DB, but in your closure you need to add the new items and of course reload your UITableView to synchronize the data in your app, see the following code to see a sample of how to do it with a sample data type too:
var items = [GroceryItem]()
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
firebase.observeEventType(.Value, withBlock: { snapshot in
var newItems = [GroceryItem]()
for item in snapshot.children {
let itemType = GroceryItem(snapshot: item as! FDataSnapshot)
newItems.append(itemType)
}
// update your item with the new ones retrieved
self.items = newItems
// reload the data
self.tableView.reloadData()
})
}
In the below struct you can see a sample of how you can create your data type from the data returned from Firebase
GroceryItem
struct GroceryItem {
let key: String!
let name: String!
let addedByUser: String!
let ref: Firebase?
var completed: Bool!
// Initialize from arbitrary data
init(name: String, addedByUser: String, completed: Bool, key: String = "") {
self.key = key
self.name = name
self.addedByUser = addedByUser
self.completed = completed
self.ref = nil
}
init(snapshot: FDataSnapshot) {
key = snapshot.key
name = snapshot.value["name"] as! String
addedByUser = snapshot.value["addedByUser"] as! String
completed = snapshot.value["completed"] as! Bool
ref = snapshot.ref
}
}
For a deeper knowledge about how to use Firebase you can read this very good tutorial:
Firebase Tutorial: Getting Started
I hope this help you.
Check that you have set your Tableview's delegate and datasource properly, to do this, go to interface builder, cmd + right click on your tableview and drag over to the yellow heading icon in interface builder.
You should see two options, 'datasource' and 'delegate', make sure that they are both checked and then rerun your app, you should see the table populate with whatever data you've loaded
You've got three issues
1) Your not populating a datasource for your tableview. This is typically an array that is stored in the class and because it's by .value you will need to iterate over those values to get to each child nodes data
2) You are observing by .value. This will return everything in the node, all children, their children etc so you won't be able to directly read it as a string value unless that's all the node contains, as in a single key:value pair, otherwise all of they key:value pairs will be read.
3) Firebase is asynchronous so within the observe block, you need to populate the array, and then re-load the tableview
Here's the solution:
Given a structure
users
user_id_0
name: "Biff"
user_id_1
name: "Buffy"
user_id_2
name: "Skip
here's the associated code to read in each name and populate a namesArray
var namesArray: [String] = []
ref.observeSingleEventOfType(.Value, withBlock: { snapshot in
for child in snapshot.children {
let name = child.value["name"] as! String
namesArray.append(name)
}
self.myTableView.reloadData()
})
substitute your items array for the namesArray.
They key is to let Firebase load the data asynchronously before telling the tableView to refresh itself, and when using .Value, ensure you iterate over all of the children in that node with snapshot.children
This is happened because there is no data in your items array. So first inside your viewDidAppear method you need to append your Firebase data dictionaries into items array and then call tableView.reloadData().
Also check your Firebase database url is correct and you need to fetch and store data in proper format while appending to items array.
Related
i have a viewcontroller with a tableview, and when user clicks on the cell, it goes to VC2. When the user has performed a action (and updated the values in VC2), i use self.dismiss(animated: true, completion: nil) to go back to the viewcontroller with the tableview, however the tableview (once the user has gone back to the tableview) is showing duplicated rows, but the child is succesfully deleted in firebase, and a new child is created - however the tableview is showing the childs that are not deleted twice.
This is all the relevant code in VC1:
class PostMessageListViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
#IBOutlet weak var table: UITableView!
var topicID:namePosts?
let currentUserID = Auth.auth().currentUser?.uid
var posts = [Post]()
lazy var refresher: UIRefreshControl = {
let refreshControl = UIRefreshControl()
refreshControl.tintColor = .white
refreshControl.addTarget(self, action: #selector(requestData), for: .valueChanged)
return refreshControl
}()
#objc
func requestData() {
self.table.reloadData()
refresher.endRefreshing()
}
func reloadData(){
table.reloadData()
}
override func viewDidLoad() {
super.viewDidLoad()
self.table.separatorStyle = UITableViewCellSeparatorStyle.none
table.refreshControl = refresher
//DataManager.shared.firstVC = self
self.table.delegate = self
self.table.dataSource = self
let postCell = UINib(nibName: "PostTableViewCell", bundle: nil)
self.table.register(postCell, forCellReuseIdentifier: "cell")
self.posts.removeAll()
Database.database().reference().child("posts").child(postID!.name)
.observe(.childAdded) { (snap) in
if snap.exists() {
//declare some values here...
self.posts.append( //some values here)
self.posts.sort(by: {$0.createdAt > $1.createdAt})
self.table.reloadData()
})
}
else {
self.table.reloadData()
}
}
//observe if a post is deleted by user
Database.database().reference().child("posts").child("posts").observe(.childRemoved) { (snapshot) in
let postToDelete = self.indexOfPosts(snapshot: snapshot)
self.posts.remove(at: postToDelete)
self.table.reloadData()
//self.table.deleteRows(at: [NSIndexPath(row: questionToDelete, section: 1) as IndexPath], with: UITableViewRowAnimation.automatic)
//self.posts.remove(at: indexPath.row)
}
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.posts.count
}
func indexOfPosts(snapshot: DataSnapshot) -> Int {
var index = 0
for post in self.posts {
if (snapshot.key == post.postID) {
return index
}
index += 1
}
return -1
}
EDIT: Forgot to say, but i have used this code in another Viewcontroller, and it works fine there. However i just copied my code from that to this one, and deleted a bunch of stuff i didn't need, however i cant seem to find what i am missing in this one.
This may not be the answer but it may lead to an answer. As noted in the comments there are two arrays being used to manage the dataSource for the tableView. One contains the data and one is using an indexing technique - I believe that may lead to issues, like the one described in the question.
The other issue is that when every child is intially added, we re-sort the array and then refresh the tableView - that can lead to delays and flicker. (flicker = bad)
So let establish a couple of things. First a class that holds the posts
PostClass {
var post_id = ""
var post_text = ""
var creation_date = ""
}
second the Firebase structure, which is similar
posts
post_id_0
text: "the first post"
timestamp: "20190220"
post_id_1
text: "the second post"
timestamp: "20190221"
then a little trick to populate the datasource and leave a child added observer. This is important as you don't want to keep refreshing the tableView with every child it as may (will) flicker. So we leverage that childAdded events always come before .value events so the array will populate, and then .value will refresh it once, and then we will update the tableView each time after. Here's some code - there's a lot going on so step through it.
var postsArray = [String]()
var initialLoad = true
func ReadPosts() {
let postsRef = self.ref.child("posts").queryOrdered(byChild: "timestamp")
postsRef.observe(.childAdded, with: { snapshot in
let aPost = PostClass()
aPost.post_id = snapshot.key
aPost.post_text = snapshot.childSnapshot("text").value as! String
aPost.creation_date = snapshot.childSnapshot("timestamp").value as! String
self.postsArray.append(aPost)
//upon first load, don't reload the tableView until all children are loaded
if ( self.initialLoad == false ) {
self.postsTableView.reloadData()
}
})
//when a child is removed, the event will contain that child snapshot
// we locate the child node via it's key within the array and remove it
// then reload the tableView
postsRef.observe(.childRemoved, with: { snapshot in
let keyToRemove = snapshot.key
let i = self.postsArray.index(where: { $0.post_id == keyToRemove})
self.postsArray.remove(at: i)
self.postsTableView.reloadData()
})
//this event will fire *after* all of the child nodes were loaded
// in the .childAdded observer. So children are sorted, added and then
// the tableView is refreshed. Set initialLoad to false so the next childAdded
// after the initial load will refresh accordingly.
postsRef.observeSingleEvent(of: .value, with: { snapshot in
self.postsTableView.reloadData()
self.initialLoad = false
})
}
Things to note
We are letting Firebase doing the heavy lifting and ordering the nodes by creation_date so they come in order.
This would be called from say, viewDidLoad, where we would set the initialLoad class var to true initially
I have an array of struct called displayStruct
struct displayStruct{
let price : String!
let Description : String!
}
I am reading data from firebase and add it to my array of struct called myPost which is initialize below
var myPost:[displayStruct] = []
I made a function to add the data from the database to my array of struct like this
func addDataToPostArray(){
let databaseRef = Database.database().reference()
databaseRef.child("Post").queryOrderedByKey().observe(.childAdded, with: {
snapshot in
let snapshotValue = snapshot.value as? NSDictionary
let price = snapshotValue?["price"] as! String
let description = snapshotValue?["Description"] as! String
// print(description)
// print(price)
let postArr = displayStruct(price: price, Description: description)
self.myPost.append(postArr)
//if i print self.myPost.count i get the correct length
})
}
within this closure if I print myPost.count i get the correct length but outside this function if i print the length i get zero even thou i declare the array globally(I think)
I called this method inside viewDidLoad method
override func viewDidLoad() {
// setup after loading the view.
super.viewDidLoad()
addDataToPostArray()
print(myPeople.count) --> returns 0 for some reason
}
I want to use that length is my method below a fucntion of tableView
public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return myPost.count --> returns 0
}
Any help would be greatly appreciated!
You making a asynchronous network request inside closure and compiler doesn't wait for the response, so just Reload Table when get post data. replace the code with below it work works fine for you. All the best.
func addDataToPostArray(){
let databaseRef = Database.database().reference()
databaseRef.child("Post").queryOrderedByKey().observe(.childAdded, with: {
snapshot in
let snapshotValue = snapshot.value as? NSDictionary
let price = snapshotValue?["price"] as! String
let description = snapshotValue?["Description"] as! String
// print(description)
// print(price)
let postArr = displayStruct(price: price, Description: description)
self.myPost.append(postArr)
print(self.myPost.count)
print(self.myPost)
self.tableView.reloadData()
//if i print self.myPost.count i get the correct length
})
}
Firebase observe call to the database is asynchronous which means when you are requesting for the value it might not be available as it might be in process of fetching it.
That's why your both of the queries to count returns 0 in viewDidLoad and DataSource delegeate method.
databaseRef.child("Post").queryOrderedByKey().observe(.childAdded, with: { // inside closure }
Inside the closure, the code has been already executed and so you have the values.
What you need to do is you need to reload your Datasource in main thread inside the closure.
databaseRef.child("Post").queryOrderedByKey().observe(.childAdded, with: {
// After adding to array
DispatchQueue.main.asyc {
self.tableView.reloadData()
}
}
I need to take variables that are populated in the viewDidLoad method to show up on labels connected to a custom cell. What i am trying to do is:
Find out SKUs in user's box stored in database
Use SKU to find out details of the product stored in database
Store product details in appropriate variable
Take said variable and populate labels in a custom table cell
The issue is that I can store the variable in the viewDidLoad method, but when I try to call the variable to populate the custom table cell, the variable is blank.
I am using Firebase to store the data. The fire base nodes are set up as the following, Node 1: Products/Sku/Item details Node 2: Box/UID/Skus
"products" : {
"0123456" : {
"brand" : "Nike",
"item_name" : "basketball"
}
},
"box" : {
"jEI5O8*****UID" : {
"sku" : "0123456"
I've been scouring through stack overflow, youtube, google, etc but i can't seem to find a solution...If you can help point me in the right direction that would be greatly appreciated! FYI I am new to swift/firebase.
import UIKit
import FirebaseAuth
import FirebaseDatabase
class drawerFaceExampleViewController: UIViewController, UITableViewDelegate,UITableViewDataSource {
var databaseRef = FIRDatabase.database().reference()
var loggedInUser = AnyObject?()
var loggedInUserData = AnyObject?()
var itemDrawer = AnyObject?()
var dataDict = AnyObject?()
#IBOutlet weak var homeTableView: UITableView!
var item_name = String()
var brand_name = String()
override internal func viewDidLoad() {
super.viewDidLoad()
self.loggedInUser = FIRAuth.auth()?.currentUser
//get the logged in users details
self.databaseRef.child("user_profiles").child(self.loggedInUser!.uid).observeSingleEventOfType(.Value) { (snapshot:FIRDataSnapshot) in
//store the logged in users details into the variable
self.loggedInUserData = snapshot
//get all the item sku's that are in the user's box
self.databaseRef.child("box/\(self.loggedInUser!.uid)").observeEventType(.ChildAdded, withBlock: { (snapshot:FIRDataSnapshot) in
let sku = snapshot.value! as! String
//access the 'products' node to extract all the item details
self.databaseRef.child("products").child(sku).observeSingleEventOfType(.Value, withBlock: { (snapshot:FIRDataSnapshot) in
if let itemvariable = snapshot.value!["item"] as? String {
self.item_name = item variable
//testing to see if item name is stored, works!
print("testing=", self.item_name)
}
if let brandvariable = snapshot.value!["brand"] as? String{
self.brand_name = brand variable
//testing to see if brand name is stored, works!
print("testingBrand =", self.brand_name)
}
})
self.homeTableView.insertRowsAtIndexPaths([NSIndexPath(forRow:0,inSection:0)], withRowAnimation: UITableViewRowAnimation.Automatic)
}){(error) in
print(error.localizedDescription)
}
}
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell: drawerFaceExampleTableViewCell = tableView.dequeueReusableCellWithIdentifier("drawerFaceExampleCell", forIndexPath: indexPath) as! drawerFaceExampleTableViewCell
//checking to see the item & brand name has been extracted...but blank :(
print("item_name=",self.item_name)
print("item_name=",self.item_name)
//this is where item & brand name extracted from viewDidLoad to display in the cell.
cell.configure(nil, brandName: brand_name, itemName: item_name)
return cell
}
}
Sounds like your data hasn't finished loading yet when you go to read the variable. You need to update your UI after the download is complete, in the completion handler:
if let itemvariable = snapshot.value!["item"] as? String {
self.item_name = item variable
//testing to see if item name is stored, works!
print("testing=", self.item_name)
}
if let brandvariable = snapshot.value!["brand"] as? String{
self.brand_name = brand variable
//testing to see if brand name is stored, works!
print("testingBrand =", self.brand_name)
}
// Update UI here.
I am able to retrieve results from a Firebase query but I am having trouble retrieving them as a dictionary to populate a tableView. Here's how I'm storing the query results:
var invites: Array<FIRDataSnapshot> = []
func getAlerts(){
let invitesRef = self.rootRef.child("invites")
let query = invitesRef.queryOrderedByChild("invitee").queryEqualToValue(currentUser?.uid)
query.observeEventType(.Value, withBlock: { snapshot in
self.invites.append(snapshot)
print(self.invites)
self.tableView.reloadData()
})
}
Printing self.invites returns the following:
[Snap (invites) {
"-KKQWErkyuehmbxom8NO" = {
invitedBy = T2k7Bm9G9RNLcHLvLlKApRbnas23;
invitee = dRJ1FqctSfTlLF8iO2ddlc9BANJ3;
role = guardian;
};
}]
I'm having trouble populating the tableView. The cell labels don't show anything:
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("cell", forIndexPath: indexPath)
let inviteDict = invites[indexPath.row].value as! [String : AnyObject]
let role = inviteDict["role"] as? String
cell.textLabel!.text = role
return cell
}
Any ideas?
Thanks!
EDIT: Printed the dictionary to console after my function is run, and it's printing []. Why is it losing it's values?
override func viewWillAppear(animated: Bool) {
getAlerts() // inside of function prints values
print(self.invites) //prints []
}
EDIT 2: Paul's solution worked!! I added the following function to have Firebase "listen" for results! I may have to edit this to only show alerts for the logged in user, but this has at least pointed me in the right direction:
override func viewWillAppear(animated: Bool) {
getAlerts()
configureDatabase()
}
func configureDatabase() {
// Listen for new messages in the Firebase database
let ref = self.rootRef.child("invites").observeEventType(.ChildAdded, withBlock: { (snapshot) -> Void in
self.invites.append(snapshot)
self.tableView.insertRowsAtIndexPaths([NSIndexPath(forRow: self.invites.count-1, inSection: 0)], withRowAnimation: .Automatic)
})
}
Check out the friendlychat Firebase codelab for an example of populating a table view from an asynchronous call. Specifically, see viewDidLoad and configureDatabase in FCViewController.swift.
Your array will always return [ ] because Firebase is async. If you add var invites: Array<FIRDataSnapshot> = [] inside of the Firebase closure you will print the data you are looking for, but im assumming you would like to use the variable outside of the closure. in order to complete that you must create a completion within getAlerts() function. If you are not sure how to complete that do a google search and that will solve your question.
in my viewDidLoad() i print out a the result of a function
override func viewDidLoad() {
super.viewDidLoad()
print("top count = \(getCurrentOrderNum())")
}
The function computes the value likes so
func getCurrentOrderNum() -> Int{
var orderNum = 0
ref = Firebase(url: "urlhiddenforprivacy")
ref.observeSingleEventOfType(.Value, withBlock: { snapshot in
let count = snapshot.childrenCount
orderNum = Int(count)
})
return orderNum
}
Yet it still prints 0? I tried to put var orderNum: Int = Int() at the top of my code instead of inside my getCurrentOrderNum function, but that didn't work. I know it gets the correct value inside my ref.observe function because when I ran this... it printed out the right value
ref.observeSingleEventOfType(.Value, withBlock: { snapshot in
let count = snapshot.childrenCount
orderNum = Int(count)
print(orderNum) //*****THIS PRINTS THE RIGHT VALUE****
})
return orderNum
}
You are returning orderNum from the method getCurrentOrderNum() before the asynchronous block actually runs. So at the time of return, orderNum is still 0, the initial value you set. The block completes later.
Your best option is probably to change the method to:
func getCurrentOrderNum(callback:Int->()) {
var orderNum = 0
ref = Firebase(url: "urlhiddenforprivacy")
ref.observeSingleEventOfType(.Value, withBlock: { snapshot in
let count = snapshot.childrenCount
orderNum = Int(count)
callback(orderNum)
})
}
And you would then call it like this:
override func viewDidLoad() {
super.viewDidLoad()
getCurrentOrderNum { orderNum in print(orderNum) }
}
This changes the getCurrentOrderNum() method to call back to a closure once it has finished retrieving the right value.
UPDATE: Based on comment below, the goal is to do something like this:
func tableView(tableView: UITableView, numberOfRowsInSection section: Int)->Int {
return getCurrentOrderNum()
}
Here is an asynchronous approach for doing that:
class YourViewController : UIViewController, UITableViewDataSource {
private var orderNumber:Int = 0
private IBOutlet var tableView:UITableView!
func getCurrentOrderNum(callback:Int->()) {
ref = Firebase(url: "urlhiddenforprivacy")
ref.observeSingleEventOfType(.Value, withBlock: { snapshot in
let count = snapshot.childrenCount
callback(Int(count))
})
}
override func viewDidLoad() {
super.viewDidLoad()
getCurrentOrderNum {
orderNum in
//This code runs after Firebase returns the value over the network
self.orderNumber = orderNum // Set our orderNumber to what came back from the request for current order number
self.tableView.reloadData() // Now reload the tableView so it updates with the correct number of rows
}
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.orderNumber // When the view first loads, this will be 0 and the table will show nothing. After the request to Firebase returns the value, this will be set to the right number, the table view will be reloaded, and it will call this method again to get the updated number of rows to display.
}
}
In general using Firebase to return function results can be tricky - it's taking an asynchronous process and squeezing it into a synchronous process. While it can be done (as shown by Daniels good answer) there are alternatives. By looking at the code in the question, there may be a couple of important Firebase concepts that may have been overlooked.
I want to present a super simple asynchronous solution that leverages the power of Firebase.
Here's some conceptual stuff:
Define a var to keep track of the order number -
var currentOrderNumber = Int
Firebase structure
orders
order_id_00
order_num: 12345
order_id_01
order_num: 12346
Set up an observer on the orders node in viewDidLoad to notify the app when a new order is added. This will occur any time an order is written to the node so then all of the clients know what the current order numbers is:
ref.queryOnOrdersNode.childAdded { snapshot in
if let orderNumber = snapshot.value["order_num"] as? Int {
currentOrderNumber = orderNumber
}
}
and from then on whenever currentOrderNumber is printed, it will contain the actual currentOrderNumber.
It's pretty cool in that you are letting Firebase do the heavy lifting; instead of polling Firebase over and over to get the currentOrderNumbers, Firebase will tell your app what the current order number is when it changes.
You can expand on this to populate a tableView and keep it updated with additions.
firebase structure
people
person_id_0
name: "Bill"
person_id_1
name: "Larry"
and the code to populate an array and add an observer for future additions:
var namesArray = [String]
peopleNode.observeEventType(.ChildAdded) { snapshot in
if let name = child.value["name"] as? String {
namesArray.append(name)
self.tableView.reloadData
}
}
and the tableView delegate method
func tableView(tableView: UITableView, numberOfRowsInSection section: Int)->Int {
return peopleArray.count
}
Again, this lets Firebase do the heavy lifting; you don't have to poll for data as when a new person is added to the people node, Firebase tells your app and the tableView is automatically updated.
You'll notice that the code is super short and tight because your letting Firebase do most of the work for you keeping your variables updated and table populated with fresh data.
(there are typos in this code as it's conceptual)