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
Related
I'm new to this so have just been learning about completion blocks, but I am unsure of how to do so in such a way that I get the data to then be apart of a tableview. I have seen other questions related, but regarding older versions of Swift.
I want the table view to contain all the fruit names collected from my database.
I have initialised an empty array list like so:
var fruitNames : [String] = []
Then fetch the data from my firestore database
func getNames(){
let db = Firestore.firestore()
db.collection("fruits").getDocuments() {(snapshot, error) in
if let error = error {
print("There was an error!")
} else {
for document in snapshot!.documents {
let name = document.get("name") as! String
self.fruitNames.append(name)
//completion needed
}
}
}
}
}
I have an extension added on for my tableView
extension FruitsViewController: UITableViewDataSource, UITableViewDelegate {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
fruitNames.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell")
cell?.textLabel?.text = self.fruitNames[indexPath.row]
return cell!
}
}
Inside your completion block, you need to tell your table view to update by calling the reloadData() method. In your class, you should have a variable holding your tableView. So, your getName should look like
func getNames(){
let db = Firestore.firestore()
db.collection("fruits").getDocuments() {(snapshot, error) in
if let error = error {
print("There was an error!")
} else {
for document in snapshot!.documents {
let name = document.get("name") as! String
self.fruitNames.append(name)
//completion needed
}
self.tableView.reloadData()
}
}
}
First of all, use [weak self] in closure. Otherwise it can lead to memory leak or crashes. You should read about
automatic reference counting and memory management
closures (https://docs.swift.org/swift-book/LanguageGuide/Closures.html)
If you want to display fruit names, you should call .reloadData() on your tableView object. Then, all delegate methods like numberOfRowsInSection or cellForRowAt will be called again.
You can do something like this :
You have to take an escaping closure as a parameter to the getName() method, which would return Void :
func getName(onComplition: #escaping (_ isSuccess: Bool, _ dataList: [String]) -> Void) {
let db = Firestore.firestore()
db.collection("fruits").getDocuments() {(snapshot, error) in
if let error = error {
print("There was an error!")
onComplition(true, [])
} else {
var data = [String]()
for document in snapshot!.documents {
let name = document.get("name") as! String
data.append(name) // Here data is local variable.
}
onComplition(true, data)
}
}
}
in ViewDidLoad()
override func viewDidLoad() {
self.getName { [weak self] (isSuccess, dataList) in
guard let weakSelf = self else { return }
weakSelf.fruitNames = dataList // fruitNames is your TableViewController's instance variable
weakSelf.tableView.reloadData()
}
}
I have written it directly in IDE, please ignore if there's any syntax error
If you have written perfect code to fetch fruit names.
But your table view is already initialized and loaded with default/empty items in the table view.
You have fetched data after the table view loaded.
So solution is you have to reload your table view again.
So in the closure (After fetching and appending your data to an array) just reload the table view like below and it reloads fresh data.
tableView.reloadData()
User [weak self] or [unowned self] for closure to avoid retain cycles and it causes memory issues.
This question has already been asked numerous times, but nothing seems to be working in my particular case. As you can see in my code below, I have created an extension of my View Controller for my UITableViewDataSource. I am trying to pull information from my Firebase Realtime Database and depending on what I get, return a certain Int. The issue is that whenever I try and return an Int from within the Firebase Snapshot, I receive this error: "Unexpected non-void return value in void function." I understand the reason for this, but I am not sure how I can make my code work. Any solutions?
My code:
extension LikeOrDislikeViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
let uid = Auth.auth().currentUser?.uid
Database.database().reference().child("Num Liked").child(uid!).observeSingleEvent(of: .value, with: { (snapshot) in
if let dictionary = snapshot.value as? [String: AnyObject] {
let numMoviesLiked = ((dictionary["Number of Movies Liked"] as? Int))!
if numMoviesLiked%4 == 0 {
return numMoviesLiked/4
}
}
})
return 10
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = UITableViewCell()
cell.backgroundColor = UIColor.red
tableView.rowHeight = 113
cell.textLabel?.text = "\(indexPath.row)"
return cell
}
}
Your problem is this return numRows inside the database block , it doesn't consider to return any values , also the call is asynchronous so don't expect it to finish before the return 10 of numberOfRowsInSection, completely remove this part and put it inside viewDidLoad
Database.database().reference().child("Num Liked").child(uid!).observeSingleEvent(of: .value, with: { (snapshot) in
if let dictionary = snapshot.value as? [String: AnyObject] {
let numMoviesLiked = ((dictionary["Number of Movies Liked"] as? Int))!
if numMoviesLiked/4 == 0 {
numRows = numMoviesLiked/4
self.tableView.reloadData()
}
}
})
logically if you do if only when you compare numMoviesLiked/4 == 0 then numRows will be always 0
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.
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
}
I'm trying to get search results to display on a tableView. I believe I have correctly parsed the JSON, the only problem is that the results won't display on my tableView.
Here is the code:
var searchText : String! {
didSet {
getSearchResults(searchText)
}
}
var itemsArray = [[String:AnyObject]]()
override func viewDidLoad() {
super.viewDidLoad()
self.tableView.delegate = self
self.tableView.dataSource = self
self.tableView.reloadData()
}
// MARK: - Get data
func getSearchResults(text: String) {
if let excapedText = text.stringByAddingPercentEncodingWithAllowedCharacters(NSCharacterSet.URLQueryAllowedCharacterSet()) {
Alamofire.request(.GET, "https://api.duckduckgo.com/?q=\(excapedText)&format=json")
.responseJSON { response in
guard response.result.error == nil else {
// got an error in getting the data, need to handle it
print("error \(response.result.error!)")
return
}
let items = JSON(response.result.value!)
if let relatedTopics = items["RelatedTopics"].arrayObject {
self.itemsArray = relatedTopics as! [[String:AnyObject]]
}
if self.itemsArray.count > 0 {
self.tableView.reloadData()
}
}
}
}
// MARK: - Table view data source
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 6 // itemsArray.count
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("SearchResultCell", forIndexPath: indexPath) as! SearchResultCell
if itemsArray.count > 0 {
var dict = itemsArray[indexPath.row]
cell.resultLabel?.text = dict["Text"] as? String
} else {
print("Results not loaded yet")
}
return cell
}
If I had a static API request I think this code would work because I could fetch in the viewDidLoad and avoid a lot of the .isEmpty checks.
When I run the program I get 6 Results not loaded yet (from my print in cellForRowAtIndexPath).
When the completion handler is called response in, it goes down to self.items.count > 3 (which passes) then hits self.tableView.reloadData() which does nothing (I checked by putting a breakpoint on it).
What is the problem with my code?
Edit
if self.itemsArray.count > 0 {
dispatch_async(dispatch_get_main_queue(), { () -> Void in
self.tableView.reloadData()
})
}
Tried this but the tableView still did not reload even though its reloading 6 times before the alamofire hander is called...
Here is the strange thing, obviously before the hander is called my itemsArray.count is going to be 0 so that's why I get Results not loaded yet. I figured out why it repeats 6 times though; I set it in numberOfRowsInSection... So #Rob, I can't check dict["Text"] or cell.resultLabel?.text because they're never getting called. "Text" is correct though, here is the link to the JSON: http://api.duckduckgo.com/?q=DuckDuckGo&format=json&pretty=1
Also, I do have the label linked up to a custom cell class SearchResultCell
Lastly, I am getting visible results.
Two problems.
One issue is prepareForSegue:
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
let searchResultTVC = SearchResultsTVC()
searchResultTVC.searchText = searchField.text
}
That's not using the "destination" view controller that was already instantiated, but rather creating a second SearchResultsTVC, setting its searchText and then letting it fall out of scope and be deallocated, losing the search text in the process.
Instead, you want:
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if let searchResultTVC = segue.destination as? SearchResultsTVC {
searchResultTVC.searchText = searchField.text
}
}
You shouldn't rely on didSet in the destination view controller to trigger the search, because that property is getting set by source view controller before the table view has even been instantiated. You do not want to initiate the search until view has loaded (viewDidLoad).
I would advise replacing the didSet logic and just perform search in viewDidLoad of that SearchResultsTVC.
My original answer, discussing the code provided in the original question is below.
--
I used the code originally provided in the question and it worked fine. Personally, I might streamline it further:
eliminate the rid of the hard coded "6" in numberOfRowsInSection, because that's going to give you false positive errors in the console;
the percent escaping not quite right (certain characters are going to slip past, unescaped); rather than dwelling on the correct way to do this yourself, it's better to just let Alamofire do that for you, using parameters;
I'd personally eliminate SwiftyJSON as it's not offering any value ... Alamofire already did the JSON parsing for us.
Anyway, my simplified rendition looks like:
class ViewController: UITableViewController {
var searchText : String!
override func viewDidLoad() {
super.viewDidLoad()
getSearchResults("DuckDuckGo")
}
var itemsArray: [[String:AnyObject]]?
func getSearchResults(text: String) {
let parameters = ["q": text, "format" : "json"]
Alamofire.request("https://api.duckduckgo.com/", parameters: parameters)
.responseJSON { response in
guard response.result.error == nil else {
print("error \(response.result.error!)")
return
}
self.itemsArray = response.result.value?["RelatedTopics"] as? [[String:AnyObject]]
self.tableView.reloadData()
}
}
// MARK: - Table view data source
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return itemsArray?.count ?? 0
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "SearchResultCell", for: indexPath) as! SearchResultCell
let dict = itemsArray?[indexPath.row]
cell.resultLabel?.text = dict?["Text"] as? String
return cell
}
}
When I did that, I got the following:
The problem must rest elsewhere. Perhaps it's in the storyboard. Perhaps it's in the code in which searchText is updated that you didn't share with us (which triggers the query via didSet). It's hard to say. But it doesn't appear to be a problem in the code snippet you provided.
But when doing your debugging, make sure you don't conflate the first time the table view delegate methods are called and the second time they are, as triggered by the responseJSON block. By eliminating the hardcoded "6" in numberOfRowsInSection, that will reduce some of those false positives.
I think you should edit :
func getSearchResults(text: String) {
if let excapedText = text.stringByAddingPercentEncodingWithAllowedCharacters(NSCharacterSet.URLQueryAllowedCharacterSet()) {
Alamofire.request(.GET, "https://api.duckduckgo.com/?q=\(excapedText)&format=json")
.responseJSON { response in
guard response.result.error == nil else {
// got an error in getting the data, need to handle it
print("error \(response.result.error!)")
return
}
let items = JSON(response.result.value!)
if let relatedTopics = items["RelatedTopics"].arrayObject {
self.itemsArray = relatedTopics as! [[String:AnyObject]]
// if have result data -> reload , & no if no
if self.itemsArray.count > 0 {
self.tableView.reloadData()
}
}else{
print("Results not loaded yet")
}
}
}
}
And
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("SearchResultCell", forIndexPath: indexPath) as! SearchResultCell
// i 'm sure: itemsArray.count > 0 in here if in numberOfRowsInSection return itemsArray.count
var dict = itemsArray[indexPath.row]
cell.resultLabel?.text = dict["Text"] as? String
return cell
}
And you should share json result(format) ,print dict in cellForRowAtIndexPath, so it s easy for help