Returning a variable outside closure - ios

I want to check the number of posts in my database and return it as the numberOfRows in my tableView. However, the below code does not work. It returns 1 everytime. I know it's because I'm setting the var requestPostCount inside a closure, but I'm not sure on how to fix it.
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
var requestPostcount: Int = 1
if segmentOutlet.selectedSegmentIndex == 0 {
// This is temporary
return 1
}
else {
// Query the database to check how many posts are there
ref.child("Request Posts").observe(.value, with: { (snapshot) in
var requestPostCount = Int(snapshot.childrenCount)
// If database is empty, only 1 row is needed to display error message
if requestPostCount == 0 {
requestPostCount = 1
}
})
}
return requestPostcount
}

numberOfRowsInSection is the wrong place to be querying your database.
At any rate, the method will return requestPostcount with your default value before the closure finishes executing.
You need to find a better place to query your database so that when numberOfSections is called, the data is already available.

You misunderstand how async methods work. You can't do an async query of a remote database in your numberOfRows method and return a value.
It's not possible to have a function that calls an async method return the results of the async method as a function result.
You need to set up your model to have no data in it, send the database query, parse the results, and then, in the completion closure, update your model and then, on the main thread, tell the table view to update.

You need to load data in ViewDidLoad . after getting data then reload tableview,
var requestPostcount: Int = 1 // declared variable
override func viewDidLoad() {
super.viewDidLoad()
ref.child("Request Posts").observe(.value, with: { (snapshot) in
var requestPostCount = Int(snapshot.childrenCount)
// If database is empty, only 1 row is needed to display error message
if requestPostCount == 0 {
requestPostCount = 1
}
tblView.reloadData()
})
}
Now numberOfRowsInSection as below
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if segmentOutlet.selectedSegmentIndex == 0 {
return 1
}
else {
return requestPostcount
}

Related

Cannot return row count after firebase data

I am trying to display different data in a tableView, according to what type of user is logged in. If the typeOfUser is set to admin, it should display certain data. If it is anything else, it should display something else.
I get the error Unexpected non-void return value in void function on return lines inside the if typeOfUser == "admin".
Here is my code:
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// #warning Incomplete implementation, return the number of rows
if Auth.auth().currentUser != nil {
let user = Auth.auth().currentUser!
ref.child("users").child(user.uid).observeSingleEvent(of: .value, with: { (snapshot) in
// Get user value
let value = snapshot.value as? NSDictionary
let typeOfUser = value?["typeOfUser"] as? String ?? ""
if typeOfUser == "admin" {
return self.loggedInAdminsMenu.count // ERROR HERE
} else {
return self.loggedInMenu.count // ERROR HERE
}
}) { (error) in
print(error.localizedDescription)
}
} else {
return notLoggedInMenu.count
}
}
In your case, you aren't returning from your function, you are returning from the closure observeSingleEvent which is a void function. Instead, you can execute the code in viewDidLoad and instead of returning, you can assign the value to a variable and call tableView.reloadData(). Don't forget to also change the numberOfRows(inSection:) function to return this newly created variable.
Another Approach would be to call a function with a closure in view did load which returns you the appropriate menu count. Then reload the table view. This way is more clean and reusable.
Function
private func getMenuCountByUser(completion: #escaping (Int) ->() ) {
//your code of getting type of user
if Auth.auth().currentUser != nil {
let user = Auth.auth().currentUser!
ref.child("users").child(user.uid).observeSingleEvent(of: .value, with: { (snapshot) in
// Get user value
let value = snapshot.value as? NSDictionary
let typeOfUser = value?["typeOfUser"] as? String ?? ""
if typeOfUser == "admin" {
//return self.loggedInAdminsMenu.count // Change HERE
completion(self.loggedInAdminsMenu.count)
} else {
//return self.loggedInMenu.count // Change HERE
completion( return self.loggedInMenu.count)
}
}) { (error) in
print(error.localizedDescription)
}
Then in your viewDidLoad
getMenuCountByUser { (menuCountFromClosue) in
//reload tableView
self.menuCount = menuCountFromClosue
tableView.reloadData()
}
Note: self.menuCount is a variable which will be given to table view numberOfRowsInASection
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int{
return self.menuCount
}
Hope it helps

UITableView Section Titles

I am attempting to create a UITableView with sections from my Parse objects. I have the Parse objects successfully. However, I am not sure how or what the best way to get the dates out of the objects to create my sections titles would be.
If I have an array of the objects should I just for through and create a date list? How would I relate that back to the date and that the object should be under that date
I was thinking of using a struct with Date and Item vars to hold the data cleaner, but I don't know I can do it that way either
Any suggestions / examples of what is the best way to do this?
EDIT:
func getExpenses() {
let query = ExpenseItem.query()
query?.order(byDescending: "expenseDate")
query?.findObjectsInBackground(block: { (expenseResult, error) in
if error == nil {
var sectionArray = [ExpenseItem]()
for object in expenseResult as! [ExpenseItem] {
if let firstObject = sectionArray.first {
if Calendar.current.compare(firstObject.expenseDate, to: object.expenseDate, toGranularity: .day) != .orderedSame {
self.tableData.append(sectionArray)
sectionArray = [ExpenseItem]()
}
} else {
sectionArray.append(object)
}
}
if sectionArray.count != 0 {
self.tableData.append(sectionArray)
}
}
})
}
Sort your array data from Parse into ascending order by date.
Declare an array of arrays of PFObject (or your PFObject subclass if you have one): var tableData: [[PFObject]]()
Loop through your Parse data, splitting it by date:
var sectionArray = [PFObject]()
// pfObjects contains the objects you retrieved from Parse, ordered by date
for object in pfObjects {
if let firstObject = sectionArray.first {
if object["dateField"] != firstObject["dateField"] {
self.tableData.append(sectionArray)
sectionArray = [PFObject]()
}
}
sectionArray.append(object)
}
if sectionArray.count != 0 {
self.tableData.append(sectionArray)
}
Now you have the data structure you need. The number of sections is the count of the outer array and the number of items in each section is the number of items in each inner array:
override func numberOfSections(in tableView: UITableView) -> Int {
return self.tableData.count
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.tableData[section].count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let item = self.tableData[indexPath.section][indexPath.row]
// deque a cell and configure it with item
}
To get the section header for each section, you can just use the date of the first object in that section's array, since all objects in the section have the same date.

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)

Array has append, but no value inside

I've declared a String array in my Swift file.
var postTitleArray : [String] = []
In my viewDidLoad method, I've append values into my array...
RestApiManager.sharedInstance.makeGetRequest("/development/vw_posts?filter=parent_term_id%20%3D%20%22%5B82%5D%22", onCompletion: { json in
for result in json["record"].arrayValue
{
let postTitle = result["post_title"].stringValue
print(postTitle)
self.postTitleArray.append(postTitle)
}
})
Other than that, I've use the count of array into the numberOfRowsInSection to set the rows number of my tableView...
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int
{
return self.postTitleArray.count
}
However, the numberOfRowsInSection has return 0, why could it happened, I've assigned value into my postTitleArray, anyone can help me take a look on this? Appreciated...
Your API call is likely not finished before numberOfRowsInSection is called.
Try calling reloadData on your UITableView instance after you have populated your postTitleArray. (I'm assuming here you have a reference to your UITableView and it's called tableView)
RestApiManager.sharedInstance.makeGetRequest("/development/vw_posts?filter=parent_term_id%20%3D%20%22%5B82%5D%22", onCompletion: { json in
for result in json["record"].arrayValue
{
let postTitle = result["post_title"].stringValue
print(postTitle)
self.postTitleArray.append(postTitle)
}
self.tableView.reloadData()
})
You should check :
if let postTitle = result["post_title"]?.stringValue{
self.postTitleArray.append(postTitle)
self.tableView.reloadData()
}

Can't retain array data outside of method in SWIFT

I am storing the category name from a JSON in an Array using alamofire .
The array has values only when it is called from this Method CategoryNameFunc.
If i call the the array from the tableview or any other method it always returns 0
CODE
var CategoryNameArray : [String] = []
override func viewDidLoad() {
super.viewDidLoad()
Network()
tester()
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return CategoryNameArray.count // This returns 0
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell : UITableViewCell = self.TableView.dequeueReusableCellWithIdentifier("cell") as! UITableViewCell
println(self.CategoryNameArray[indexPath.row])
cell.textLabel?.text = "Hello"
return cell
}
func Network(){
Alamofire.request(.GET, "http://www.wive.com/index.php/capp/category_list")
.responseJSON { (_, _, data, _) in
let json = JSON(data!)
let count = json.count
self.CategoryNameFunc(json, Count: count) }
}
func CategoryNameFunc(Json: JSON, Count: Int)
{
for index in 0...Count-1 {
let name = Json[index]["CATEGORY_NAME"].string
CategoryNameArray.append(name!)
}
// This returns 23 (The correct value)
println(self.CategoryNameArray.count)
}
When you called Network() function it creates a new thread (Alamofire start an asynchronous request) and your tester() function is not waiting for your Network() function to finish before you count your CategoryNameArray().But your CategoryNameFunc() function waits for network operation to finish.
I am not sure (didn't use Almofire) but it think, this happens because the method Network, more precisly the Almofire request is fired asynchronously.
So, the methods Network() and tester() are running simultaneously, but because Network() needs to fetch data first, tester() is faster and is executed first.
The proper way to execute tester() and Network() one after another would be:
func CategoryNameFunc(Json: JSON, Count: Int)
{
for index in 0...Count-1 {
let name = Json[index]["CATEGORY_NAME"].string
CategoryNameArray.append(name!)
}
// run tester, AFTER you have the data.
tester()
}

Resources