Unable to reload tableview properly swift ios - ios

I have a tableview which is part of a navigation controller that returns a list of questions.
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for:indexPath) as! AnswerTableViewCell
let data = quesList[indexPath.row]
cell.labelName.text = data.sender_name;
cell.labelQuestion.text = data.text;
return cell
}
func fetchQues(){
Ref.queryOrdered(byChild: currentuser).queryEqual(toValue: nil).observe(DataEventType.value, with:{(snapshot)in
if snapshot.childrenCount>0{
self.quesList.removeAll()
for data in snapshot.children.allObjects as![DataSnapshot]{
let dataObject = data.value as? [String: AnyObject]
let userid = dataObject?["sender_id"];
let username = dataObject?["sender_name"];
let questext = dataObject?["text"];
let questionid = dataObject?["id"];
let data = Question(id: questionid as! String, sender_id: userid as! String, sender_name: username as! String, text: questext as! String)
self.quesList.insert(data, at :0)
}
self.AnswerTable.reloadData()
}
})
}
I then have a didselectrow function to segue to a VC part of this navigation stack that lets the user input their answer for the question.
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let data = quesList[indexPath.row]
questionselect1 = data.text;
questionid1 = data.id;
performSegue(withIdentifier: self.questSegue, sender: nil)
}
After the user submit the answer to the question, it adds a dictionary into the question dictionary and returns to the list of question screen. The queryorder(bychild:currentuser).queryeuqual(tovalue:nil) would only return questions that have not been answered by the current user. So everytime a user answers a question, that question would get removed from the list.
#IBAction func submitAnswer(_ sender: Any) {
if questiontext.text != "" {
DBProvider.Instance.postAnswers(senderID: userid, senderName: username2, text: questiontext.text!)
DBProvider.Instance.postAnswered()
self.questiontext.text = "";
self.navigationController!.popViewController(animated: true)
//append data to the current question
} else {
alertTheUser(title: "Please Submit A Valid Answer", message: "Question Field Cannot Be Empty");
}
}
This works fine at the moment, however, when there is only one question on the list, and the user answers it, and returns to the questions tableview, the last question does not get removed from the list, which it should be since it has been answered.
I just have to add that when I log out of my app completely and login again, the questionlist is empty and is refreshed properly, since all the question have been answered.
I have been trying to figure out why this is happening by changing the segueing method and reloading the view with viewwillload or viewdidload and it does not work.
Hopefully someone understands what I am saying and can provide a answer to my bug.

Without looking at your code it's hard to say exactly but this could be caused by not having a background colour set on the root UIView of one of your View Controllers.

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.

Issue with first Firebase query item out of order until refresh

I have a firebase database to pull 50 users with the highest integer value and to display them from highest to lowest. The issue will arise when I enter the leaderboard view for the first time. The order should show jlewallen18 at the top AND THEN appledev. But on first load appledev is at the top, until I back out and open the leaderboard again (code at the bottom).
Leaderboard code:
class LeaderboardViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
#IBOutlet weak var leaderboardTableView: UITableView!
var userModel : [User] = [User]()
let pipeline = ImagePipeline {
//config settings for image display removed for brevity
}
override func viewDidLoad() {
super.viewDidLoad()
leaderboardTableView.delegate = self
leaderboardTableView.dataSource = self
fetchUsers()
}
func fetchUsers() {
let queryRef = Database.database().reference().child("users").queryOrdered(byChild: "ranking").queryLimited(toLast: 50)
queryRef.observe(.childAdded, with: { (snapshot) in
if let dictionary = snapshot.value as? [String : AnyObject]{
let user = User(dictionary: dictionary)
self.userModel.append(user)
}
DispatchQueue.main.async(execute: {
self.leaderboardTableView.reloadData()
})
})
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return userModel.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "leaderboardTableCell", for: indexPath) as! LeaderboardTableCell
if userModel.count > indexPath.row {
if let profileImageURL = userModel[indexPath.row].photoURL {
let url = URL(string: profileImageURL)!
var options = ImageLoadingOptions()
options.pipeline = pipeline
options.transition = .fadeIn(duration: 0.25)
Nuke.loadImage(
with: ImageRequest(url: url).processed(with: _ProgressiveBlurImageProcessor()),
options: options,
into: cell.userImage
)
}
}
cell.userName.text = userModel[indexPath.row].username
cell.watchTime.text = "\(String(describing: userModel[indexPath.row].watchTime!))"
cell.ranking.text = "\(indexPath.row + 1)"
cell.userImage.layer.cornerRadius = cell.userImage.frame.size.width/2
return cell
}
}
I thought it might be because I am using the same model name userModel in both my Profile page view and my Leaderboard view but when i changed the model name in my leaderboard view nothing changed. What else can I share to help? Thanks!
EDIT: here's my console output after printing out watchTime which is the integer I have rankings for:
HERES WHERE I OPEN LEADERBOARD PAGE FIRST:
Optional(28)
Optional(247)
Optional(0)
Optional(0)
Optional(0)
Optional(0)
AFTER I GO BACK AND CLICK TO VIEW LEADERBOARD AGAIN:
Optional(247)
Optional(28)
Optional(0)
Optional(0)
Optional(0)
Optional(0)
The issue here is related to this line of code...
let queryRef = Database.database().reference().child("users").queryOrdered(byChild: "ranking").queryLimited(toLast: 50)
Changing this limit to 10 makes the app work as expected, which is a temporary 'fix'.
If we figure out why that limit is causing issues I'll be sure to update this answer.

Issue trying to complete Firebase Storage download before showing tableview

I have a table view where depending on the cell class it will download an image from Firebase. I've noticed when using the app that cells with the same cell identifier will show the previous downloaded image before showing the new one. This is what I have before changing it.
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if tableData[indexPath.row]["Image"] != nil {
let cell = tableView.dequeueReusableCell(withIdentifier: "imageNotesData", for: indexPath) as! ImageNotesCell
cell.notes.delegate = self
cell.notes.tag = indexPath.row
cell.notes.text = tableData[indexPath.row]["Notes"] as! String
guard let imageFirebasePath = tableData[indexPath.row]["Image"] else {
return cell }
let pathReference = Storage.storage().reference(withPath: imageFirebasePath as! String)
pathReference.getData(maxSize: 1 * 1614 * 1614) { data, error in
if let error = error {
print(error)
} else {
let image = UIImage(data: data!)
cell.storedImage.image = image
}
}
return cell
}
else {
let cell = tableView.dequeueReusableCell(withIdentifier: "notesData", for: indexPath) as! NotesCell
//let noteString = tableData[indexPath.row]["Notes"] as! String
cell.notes.text = tableData[indexPath.row]["Notes"] as! String
cell.notes.delegate = self
cell.notes.tag = indexPath.row
return cell
}
}
Knowing that this is not a good user experience and that it looks clunky, I tried to move the pathReference.getData to where I setup the data but the view appears before my images finish downloading. I have tried to use a completion handler but I'm still having issues.
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(true)
getSectionData(userID: userID, city: selectedCity, completion: {(sectionString) in
self.setupTableCellView(userID: userID, city: selectedCity, section: sectionString) { (tableData) in
DispatchQueue.main.async(execute: {
self.cityName?.text = selectedCity
self.changeSections.setTitle(sectionString, for: .normal)
self.currentSectionString = sectionString
self.setupTableData(tableDataHolder: tableData)
})
}
})
}
func setupTableCellView(userID: String, city: String, section: String, completion: #escaping ([[String:Any]]) -> () ) {
let databaseRef = Database.database().reference().child("Users").child(userID).child("Cities").child(city).child(section)
var indexData = [String:Any]()
var indexDataArray = [[String:Any]]()
databaseRef.observeSingleEvent(of: .value, with: { (snapshot) in
for dataSet in snapshot.children {
let snap = dataSet as! DataSnapshot
//let k = snap.key
let v = snap.value
indexData = [:]
for (key, value) in v as! [String: Any] {
//indexData[key] = value
if key == "Image" {
//let pathReference = Storage.storage().reference(withPath: value as! String)
print("before getImageData call")
self.getImageData(pathRef: value as! String, completion: {(someData) in
print("before assigning indexData[key]")
indexData[key] = someData
print("after assigning indexData[key]")
})
} else {
indexData[key] = value
}
}
indexDataArray.append(indexData)
}
completion(indexDataArray)
})
}
func getImageData(pathRef: String, completion: #escaping(UIImage) -> ()) {
let pathReference = Storage.storage().reference(withPath: pathRef as! String)
pathReference.getData(maxSize: 1 * 1614 * 1614, completion: { (data, error) in
if let error = error {
print(error)
} else {
let image = UIImage(data:data!)
print("called before completion handler w/ image")
completion(image!)
}
})
}
I don't know if I am approaching this the right way but I think I am. I'm also guessing that the getData call is async and that is why it will always download after showing the table view.
You can't do this.
Make the request from Firebase.
Over time, you will get many replies - all the information and all the changing information.
When each new item arrives - and don't forget it may be either an addition or deletion - alter your table so that it displays all the current items.
That's OCC!
OCC is "occasionally connected computing". A similar phrase is "offline first computing". So, whenever you use any major service you use every day like Facebook, Snapchat, etc that is "OCC": everything stays in sync properly whether you do or don't have bandwidth. You know? The current major paradigm of device-cloud computing.
Edit - See Fattie's comments about prepareForReuse()!
With reusable table cells, the cells will at first have the appearance they do by default / on the xib. Once they're "used", they have whatever data they were set to. This can result in some wonky behavior. I discovered an issue where in my "default" case from my data, I didn't do anything ecause it already matched the xib, but if the data's attributes were different, I updated the appearance. The result was that scrolling up and down really fast, some things that should have had the default appearance had the changed appearance.
One basic solution to just not show the previous image would be to show a place holder / empty image, then call your asynchronous fetch of the image. Not exactly what you want because the cell will still show up empty...
Make sure you have a local store for the images, otherwise you're going to be making a server request for images you already have as you scroll up and down!
I'd recommend in your viewDidLoad, call a method to fetch all of your images at once, then, once you have them all, in your success handler, call self.tableview.reloadData() to display it all.

Array index Error when updating or removing data from Firebase

I have an error that occurs when I add or delete data from a node in firebase. When the data is added or deleted on firebase I get an array index out of bounds error. My array has two items, but the tableView thinks that there are three items and thus tries to access a value that doesn't exist. I can't figure out how to prevent this. For more context I am using an alertView with a closure that performs the adding or deleting of information in firebase. This alertView is in the didSelectCellAtIndexPath method. The error is occurring in the cellForRowAtIndexPath Method when accessing the array like so user.id = self.bookRequestors[indexPath.row]
Here is some of the code I wrote:
`
alertVC.addAction(PMAlertAction(title: "Approve this users request",
style: .default, action: {
print("Book Approved")
let user = User()
user.id = self.bookRequestors[indexPath.row]
var users = self.userInfoArray[indexPath.row]
var bookRef = Database.database().reference().child("books").child("-" + self.bookID)
bookRef.observeSingleEvent(of: .value, with: { (snapshot) in
var tempDict = snapshot.value as! [String:AnyObject]
if tempDict["RequestApproved"] == nil || tempDict["RequestApproved"] as! String == "false"{
//bookRef.updateChildValues(["RequestApproved": "true", "ApprovedRequestor": user.id])
bookRef.updateChildValues(["RequestApproved": "true", "ApprovedRequestor": user.id], withCompletionBlock: { (error, ref) in
let userRef = Database.database().reference().child("users").child(user.id!).child("requestedBooks")
// true meaning this book has been approved for this user
userRef.updateChildValues(["-" + self.bookID:"true"])
})
} else{
print("Already Approved!")
let alertVC = PMAlertController(title: "Sorry?", description: "You Already Approved that book for someone Else", image: UIImage(named: "booksandcoffee.jpg"), style: .alert)
alertVC.addAction(PMAlertAction(title: "Ok", style: .default))
self.present(alertVC, animated: true, completion: nil)
}
})`
EDIT: MORE CODE FOR CONTEXT
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "people", for: indexPath) as! RequestTableViewCell
// Configure the cell...
var users = self.userInfoArray[indexPath.row]
var myName = users["name"]
var myPic = users["profileImageUrl"]
let user = User()
print("This is a count: \(self.bookRequestors.count)")
print ("the index is: \(indexPath)")
//This is the array throwing the error, but this array is populated from the previous view and is not modified afterwards.
user.id = self.bookRequestors[indexPath.row]
cell.userImage?.setRounded()
cell.userImage.clipsToBounds = true
let processor = RoundCornerImageProcessor(cornerRadius: 100)
DispatchQueue.main.async {
cell.userImage.loadImageUsingCacheWithUrlString(myPic as! String)
}
cell.userName.text = myName as! String
if user.id == approvedRequestorID{
cell.wasApproved.image = UIImage(named: "icons8-checked_filled.png")
cell.approvedLabel.text = "You approved to swap with: "
}
cell.backgroundColor = .clear
return cell
}
EDIT II : Here are my numberofSections and numberofRowsPerSection Methods
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 userInfoArray.count
}
EDIT III: Function where userInfoArray is updated.
func grabUsers(){
//print(bookRequestors)
for requests in bookRequestors{
let usersRef = Database.database().reference().child("users").child(requests)
usersRef.observe(.value, with: { (snapshot) in
var myDict:[String: AnyObject] = [:]
myDict = snapshot.value as! [String:AnyObject]
self.userInfoArray.append(myDict)
self.tableView.reloadData()
})
}
}
Its hard to tell what exactly is going on without seeing your code, but here is my best guess:
You are backing your cells with self.bookRequestors which you say is a static array assigned by the previous VC.
However you are using userInfoArray.count to back the number of rows in the tableView, and this value changes in your usersRef.observe method.
Specifically you are appending to userInfoArray; so userInfoArray.count strictly increases.
Therefore if the two arrays statrt at the same size, and the one that determines the count is getting bigger but the one you are indexing into is always the same size, then eventually you will index out of bounds.
Back the number of rows by the data you are actually showing in the cell.

Tableview Like Snapchat Send Feature (Multiple Selection and Swipeable Label)

In my Xcode Project I will like to have a similar view like Snapchat's "Send To..." screen (I have attached a screenshot). I have already made a tableview and populate it and have allowed multiple selection on. I am currently having trouble with two things:
1) Multiple Selection: I can select an cell I want, but when I tap on the search bar and start typing, all my previous selections go away. I am assuming that I need to add all of the names in a array and somehow communicate the array with the table so it shows if this username is in the array then make it selected in the tableview. But I am not sure how to do that. How can I do this?
2) Sending to Bottom Bar (blue in photo): As you may know, in Snapchat as you press on which users you want to send the snap to, their names get added to the bar at the bottom, as you fill up the bar, it because swipe able where you can horizontally scroll through the names you have added. I can append the names to an array and show the array in a label like theirs, but I do not know how to make it so a user can horizontally scroll through it.How do I implement this same feature?
Feel free to answer ANY of the questions! You do not need to do all of them, I just need them answered. Here's my code so far:
class User {
var userID:String?
var userFullName:String?
var userUsername:String?
var userProfileImage:PFFile?
var isPrivate:Bool
init(userID : String, userFullName : String, userUserName : String, userProfileImage : PFFile, isPrivate : Bool) {
self.userID = userID
self.userFullName = userFullName
self.userUsername = userUserName
self.userProfileImage = userProfileImage
self.isPrivate = isPrivate
}
}
var userArray = [User]()
func loadFriends() {
//STEP 1: Find friends
let friendsQuery = PFQuery(className: "Friends") //choosing class
friendsQuery.whereKey("friendOne", equalTo: PFUser.current()?.objectId ?? String()) //finding friends
friendsQuery.limit = self.page //number of users intitally showing
friendsQuery.findObjectsInBackground (block: { (objects, error) -> Void in
if error == nil { //if no error
//clean up
self.friendsArray.removeAll(keepingCapacity: false)
//STEP 2: Find related objects depending on query setting
for object in objects! {
self.friendsArray.append(object.value(forKey: "friendTwo") as! String) //hold array info of friend
}
//STEP 3: Find friend info
let query = PFUser.query()
query?.whereKey("objectId", containedIn: self.friendsArray)
query?.addDescendingOrder("createdAt") //how to order users
query?.findObjectsInBackground(block: { (objects, error) -> Void in
if error == nil {
for object in objects! {
var user : User
let fullname = (object.value(forKey: "fullname") as! String)
let username = (object.object(forKey: "username") as! String)
let profilePhoto = (object.object(forKey: "profilePhoto") as! PFFile)
let objectID = (object.objectId!)
let isPrivate = (object.object(forKey: "isPrivate") as! Bool)
user = User(userID: objectID, userFullName: fullname, userUserName: username, userProfileImage: profilePhoto, isPrivate: isPrivate)
self.userArray.append(user)
}
self.tableView.reloadData()
} else {
print(error!)
}
})
} else {
print(error!)
}
})
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell") as! FriendCell
let user = userArray[indexPath.row]
//add user info to cells
cell.fullnameLabel.text = user.userFullName
cell.usernameLabel.text = user.userUsername
cell.objectID = user.userID!
cell.isPrivate = user.isPrivate
user.userProfileImage?.getDataInBackground (block: { (data, error) in
if error == nil {
cell.profilePhoto.image = UIImage(data: data!)
}
})
})
}
1) Multiple Selection:
You should have a User class (e.g User) that holds user properties instead of maintaining array for each property. Store User object in a Array. User class could be like below:
class User {
var userID:String
var userFullName:String
var userName:String
var userProfileImageUrl:String
init(userID:String,userFullName:String,userName:String,userProfileImageUrl:String) {
self.userID = userID
self.userFullName = userFullName
self.userName = userName
self.userProfileImageUrl = userProfileImageUrl
}
}
You could have a User extension to check if that user is selected or not(e.g isSelected).
import UIKit
import Foundation
private var selectedKey: UInt8 = 0
extension User {
var isSelected:Bool{
get {
return objc_getAssociatedObject(self, &selectedKey) as! Bool
}
set {
objc_setAssociatedObject(self, &selectedKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
}
}
Now in your func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell check that user.isSelected == true/false and update your selected/deselected image accordingly.
And update the value of isSelected in func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath)
2) Sending to Bottom Bar:
For bottom bar add a UICollectionView as a subview in UIView. Create a class overriding UICollectionViewCell that holds a UILabel. You can add flow layout in UICollectionView.
I have given just an idea to start with.Hope it will help you.
I think, you set bool check for every cell in tableView. If cell load again, it will not show check. Because, It check is false.

Resources