Array of struct not updating outside the closure - ios

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()
}
}

Related

What am I doing wrong while populating this UITableView in Swift?

I am trying to populate a UITableView using an array and I am unable to do so. Here is what I have so far. This code is for retrieving data and storing it in the array that I am using to populate the UITableView:
func prepareForRetrieval() {
Database.database().reference().child("UserCart").child(Auth.auth().currentUser!.uid).observe(.value, with: {
(snapshot) in
for snap in snapshot.children.allObjects {
let id = snap as! DataSnapshot
self.keyArray.append(id.key)
}
self.updateCart()
})
}
func updateCart() {
for key in keyArray {
Database.database().reference().child("UserCart").child(Auth.auth().currentUser!.uid).child(key).observeSingleEvent(of: .value, with: {
(snapshot) in
let value = snapshot.value as? NSDictionary
let itemName = value?["Item Name"] as! String
let itemPrice = value?["Item Price"] as! Float
let itemQuantity = value?["Item Quantity"] as! Int
self.cartArray.append(CartData(itemName: itemName, itemQuantity: itemQuantity, itemPriceNumber: itemPrice))
print(self.cartArray.count)
})
}
}
The data is properly appending into the array and when I print the count of the array, it prints the correct count. This means that the data is there. However, when I try to populate a UITableView, it doesn't detect any data. I have the following code to make sure that there is data in the array before trying to populate the UITableView:
override func viewDidLoad() {
super.viewDidLoad()
cartBrain.prepareForRetrieval()
if cartBrain.cartArray.isEmpty == false{
tableViewOutlet.dataSource = self
tableViewOutlet.reloadData()
}
else {
tableViewOutlet.isHidden = true
tableViewOutlet.isUserInteractionEnabled = false
purchaseButtonOutlet.isEnabled = false
cartEmptyLabel.text = "Your cart is empty. Please add items and check back later."
}
}
When I open the View Controller, the TableView is disabled because it doesn't detect any data. I have already set the data source to self and the thing is that when the count of the array is printed, it again prints the correct amount. I have already set the data source to self for the UITableView. Here is my code for the UITableView:
extension CartViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return cartBrain.cartArray.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cartcustomcell", for: indexPath)
cell.textLabel?.text = cartBrain.cartArray[indexPath.row].itemName
cell.detailTextLabel?.text = String(cartBrain.cartArray[indexPath.row].itemQuantity)
return cell
}
}
I don't understand why the count of the array prints the correct amount meaning that there is data stored in it but when the View Controller is loaded, it detects that the array is empty. Thanks for the help and I'm sorry if the question is a bit unclear.
After appending data to cartArray in updateCart you should reloadData(), like this:
weak var tableViewOutlet: UITableView?
func updateCart() {
for key in keyArray {
Database.database().reference().child("UserCart").child(Auth.auth().currentUser!.uid).child(key).observeSingleEvent(of: .value, with: {
(snapshot) in
let value = snapshot.value as? NSDictionary
let itemName = value?["Item Name"] as! String
let itemPrice = value?["Item Price"] as! Float
let itemQuantity = value?["Item Quantity"] as! Int
self.cartArray.append(CartData(itemName: itemName, itemQuantity: itemQuantity, itemPriceNumber: itemPrice))
DispatchQueue.main.async {
self.tableViewOutlet.reloadData()
}
})
}
}
The updateCart doesn't seem to have any connection to the tableViewOutlet so you need to pass in a reference to it in your viewDidLoad like this:
override func viewDidLoad() {
super.viewDidLoad()
cartBrain.tableViewOutlet = tableViewOutlet
cartBrain.prepareForRetrieval()
Note: Since you're using a for loop to trigger the async call multiple times you can use the array count to check if all the items are appended to do the reload to avoid multiple reloads.

the retrieved data doesn't append in my array swift4

I'm trying to append "the retrieved data -Keys- from firebase" into an array but it doesn't work
This is the for loop output #2 the retrieved keys
This the keys from firebase
This is the code
let ref = Database.database().reference()
ref.child("Faculty ").observe(.value, with: { (snapshot) in
if let result = snapshot.children.allObjects as? [DataSnapshot] {
for child in result {
let FacultyName = child.key as! String
print(FacultyName)
self.NamesofFac.append(FacultyName)
}
}
})
for i in 0...self.NamesofFac.count {
print(self.NamesofFac.count)
print(" line")
print(self.NamesofFac)
The problem you are having is the Firebase Observe function give a callback in the form of a (snapshot).
It takes a bit of time to go to the web to get the data, therefore, firebase returns the data asynchronously. Therefore your code in your for loop will run before your firebase data has been returned. At the time your for loop code runs the array is still blank. But the for loop code in a separate function as you see in my sample code and call it straight after your for loop inside your firebase observe call.
Try this instead:
override func viewDidLoad() {
getFirebaseData()
}
func getFirebaseData() {
let ref = Database.database().reference()
ref.child("Faculty ").observe(.value, with: { (snapshot) in
if let result = snapshot.children.allObjects as? [DataSnapshot] {
for child in result {
let FacultyName = child.key as! String
print(FacultyName)
self.NamesofFac.append(FacultyName)
}
printNames()
}
})
}
func printNames() {
for i in 0...self.NamesofFac.count {
print(self.NamesofFac.count)
print(" line")
print(self.NamesofFac)
}
}
This was it won't print the names until they have been fully loaded from firebase.
PS: Your naming conventions are incorrect. You seem to be naming variables with a capital letter. Variables should be camel case. Classes should start with a capital.

Call reloadData() in the right place

I'm trying to fetch data from firebase and pass to tableview.
// Model
import UIKit
import Firebase
struct ProfInfo {
var key: String
var url: String
var name: String
init(snapshot:DataSnapshot) {
key = snapshot.key
url = (snapshot.value as! NSDictionary)["profileUrl"] as? String ?? ""
name = (snapshot.value as! NSDictionary)["tweetName"] as? String ?? ""
}
}
// fetch
var profInfo = [ProfInfo]()
func fetchUid(){
guard let uid = Auth.auth().currentUser?.uid else{ return }
ref.child("following").child(uid).observe(.value, with: { (snapshot) in
guard let snap = snapshot.value as? [String:Any] else { return }
snap.forEach({ (key,_) in
self.fetchProf(key: key)
})
}, withCancel: nil)
}
func fetchProf(key: String){
var outcome = [ProfInfo]()
ref.child("Profiles").child(key).observe(.value, with: { (snapshot) in
let info = ProfInfo(snapshot: snapshot)
outcome.append(info)
self.profInfo = outcome
self.tableView.reloadData()
}, withCancel: nil)
}
//tableview
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return profInfo.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "followCell", for: indexPath) as! FollowingTableViewCell
cell.configCell(profInfo: profInfo[indexPath.row])
return cell
}
However it returns one row but profInfo actually has two rows. when I implement print(self.profInfo) inside fetchProf it returns two values. But after passed to tableview, it became one. I'm not sure but I guess the reason is that I put reloadData() in the wrong place because I hit break point and reloadData() called twice. So, I think profInfo replaced by new value. I called in different places but didn't work. Am I correct? If so, where should I call reloadData()? If I'm wrong, how can I fix this? Thank you in advance!
You need to append the new data to the profinfo array. Simply replace the fetchProf method with this:-
func fetchProf(key: String){
var outcome = [ProfInfo]()
ref.child("Profiles").child(key).observe(.value, with: { (snapshot) in
let info = ProfInfo(snapshot: snapshot)
outcome.append(info)
self.profInfo.append(contentOf: outcome)
Dispatch.main.async{
self.tableView.reloadData()
}
} , withCancel: nil)
}
self.tableView.reloadData() must be called from the main queue. Try
DispatchQueue.main.async {
self.tableView.reloadData()
}
if you notice one thing in the following function you will see
func fetchProf(key: String){
var outcome = [ProfInfo]()
ref.child("Profiles").child(key).observe(.value, with: { (snapshot) in
let info = ProfInfo(snapshot: snapshot)
outcome.append(info)
//Here
/You are replacing value in self.profInfo
//for the first time when this is called it results In First profile info
//When you reload here first Profile will be shown
//Second time when it is called you again here replaced self.profInfo
//with second Outcome i.e TableView reloads and output shown is only second Profile
//you had initialised a Array self.profInfo = [ProfInfo]()
//But you just replacing array with Single value Actually you need to append data
// I think here is main issue
self.profInfo = outcome
//So try Appending data as
//self.profInfo.append(outcome) instead of self.profInfo = outcome
//Then reload TableView to get both outputs
self.tableView.reloadData()
}, withCancel: nil)
}
Table view showing one content because when table view reloaded then profile info not combine all data. You need to reload the table view after combining all data. This will help you.
// fetch
var profInfo = [ProfInfo]()
func fetchUid(){
guard let uid = Auth.auth().currentUser?.uid else{ return }
ref.child("following").child(uid).observe(.value, with: { (snapshot) in
guard let snap = snapshot.value as? [String:Any] else { return }
snap.forEach({ (key,_) in
self.fetchProf(key: key)
})
// When all key fetched completed the just reload the table view in the Main queue
Dispatch.main.async{
self.tableView.reloadData()
}
}, withCancel: nil)
}
func fetchProf(key: String){
ref.child("Profiles").child(key).observe(.value, with: { (snapshot) in
let info = ProfInfo(snapshot: snapshot)
self.profInfo.append(info) // Here just add the outcome object to profileinfo
}, withCancel: nil)
}
This way no need to handle another array.

Swift variable still 0 after assigning it a value?

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)

Swift 2 Firebase TableView

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.

Resources