My UITableView will only show all of the fully loaded contents in the cells if I follow the advice of this post and call reloadData() in the main thread with DispatchQueue. I'm doing this in tableView cellForRowAt because it wasn't fully loading the values from my array into all of the cells otherwise. Now it's constantly reloading and is obviously very energy intensive. This makes it harder to scroll in the Table View, which makes sense.
How can I prevent this ridiculous use of energy while still loading the data into the cells as I need?
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
var cell:UITableViewCell? =
charitiesSupportedTable?.dequeueReusableCell(withIdentifier: "SupportedCell")
if (cell == nil)
{
cell = UITableViewCell(style: UITableViewCellStyle.subtitle,
reuseIdentifier: "abc")
}
if(indexPath.row <= moneyRaisedOfPrevPostsArray.count-1){
cell?.textLabel?.text = charOfPrevPostsArray[indexPath.row]
cell?.detailTextLabel?.text = moneyRaisedOfPrevPostsArray[indexPath.row]
}
DispatchQueue.main.async {
self.charitiesSupportedTable.reloadData()
}
return cell!
}
Here's my viewDidLoad where I create the array
override func viewDidLoad() {
super.viewDidLoad()
let userEmail = Auth.auth().currentUser!.email
var email1 = userEmail?.replacingOccurrences(of: "#", with: "", options: NSString.CompareOptions.literal, range:nil)
email1 = email1?.replacingOccurrences(of: ".", with: "", options: NSString.CompareOptions.literal, range:nil)
charitiesSupportedTable.delegate = self
charitiesSupportedTable.dataSource = self
charitiesSupportedTable.isScrollEnabled = false
reff = Database.database().reference().child("Posts")
let userRef1 = reff?.queryOrdered(byChild: "user").queryEqual(toValue : email1)
userRef1?.observe(.value, with: { (snapshot) in
for child in snapshot.children {
let snap = child as! DataSnapshot
let key = snap.key
let value = snap.value
let valueAsDict = value as! NSDictionary
charityOfPrevPosts = valueAsDict.value(forKey: "charity") as! String
viewCountOfPrevPosts = valueAsDict.value(forKey: "viewCount") as! String // times .006
moneyRaisedPrevPosts = Double(viewCountOfPrevPosts)! * 0.006
charOfPrevPostsArray.append(charityOfPrevPosts)
moneyRaisedOfPrevPostsArray.append(String(moneyRaisedPrevPosts))
}
charOfPrevPostsArray.reverse()
moneyRaisedOfPrevPostsArray.reverse()
}
)
moneyRaisedOfPrevPostsArray.removeAll()
charOfPrevPostsArray.removeAll()
reff = Database.database().reference().child("UsersViews").child(email1!).child("totalMoneyRaised")
reff?.observe(DataEventType.value, with: { (snapshot) in
var totalMoney = snapshot.value as! Double
self.totalMoneyLabel.text = String(format: "$%.02f", totalMoney)
})
// don't know if this part is relevant but I do return charitiesCount + 2 in numberOfRowsInSection
reff = Database.database().reference()
databaseHandle = reff?.child("UsersViews").child(email1!).child("charitiesSupported").observe(.value, with: { (snapshot) in
let myDictionary11 = snapshot.value as? NSDictionary
// Adds brands and charities to myData array
for value in (myDictionary11?.allValues)! {
self.myCharities.append(value as! String)
}
charitiesCount = self.myCharities.count
})
}
Related
I am working on a group chat app and trying to fetch the last message from a node to display the last message under the group name. Everything is working fine. When the group receives a new message, the last message is showing in the correct group and some other random groups as well. If I open the correct group and come back, all the other groups are showing the correct last message. Kindly help me with the below code.
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return groupList.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell:GroupsTableViewCell = tableView.dequeueReusableCell(withIdentifier: "GroupsTableViewCell") as! GroupsTableViewCell
cell.selectionStyle = .none
var user: Groupslist
if groupList.count > 0 {
user = groupList[indexPath.row]
cell.lbl_name_group.text = user.name;
cell.view_count.isHidden = true
GetLastMsg(groupID: user.group_id ?? "", cell: cell)
cell.img_group.image = UIImage.init(named:"final_grp")
GetUnReadMsgCount(groupID: user.group_id ?? "", cell: cell)
}
return cell
}
groupList contains the list of groups which is fetched from firebase on viewWillAppear.
func GetLastMsg(groupID : String, cell : GroupsTableViewCell){
let userRef2 = rootRef2.child("message").child(groupID).queryLimited(toLast: 1)
userRef2.observe( .value, with: { snapshot in
guard let _ = snapshot.value as? [String:AnyObject] else {
print("Error")
return
}
print("children count: \(snapshot.children.allObjects.count)")
for snap in snapshot.children.allObjects as! [DataSnapshot] {
let message = Message()
let value = snap.value as? [String: Any] ?? [:]
let text = value["text"] as? String ?? "Name not found"
let mimType = value["mimType"] as? String
let name_doc = value["name_doc"] as? String ?? "file.file"
message.text = text
message.mimType = mimType
message.name_doc = name_doc
message.timestamp_group = (value["timestamp"] as! NSNumber)
print(value["idSender"] as? String)
print(message.name_doc)
var msg_last = ""
// cell.lbl_lastmsg.text = msg_last
if message.mimType == "image/jpeg" {
msg_last = "Image"
cell.img_small_width.constant=28
cell.img_small.image = UIImage(named: "im")
}else if message.mimType == "audio/3gpp" {
msg_last = "Audio"
cell.img_small_width.constant=28
cell.img_small.image = UIImage(named: "mi")
}else if message.mimType == "video/mp4" {
msg_last = "Video"
cell.img_small_width.constant=28
cell.img_small.image = UIImage(named: "vi")
}else if message.mimType == "application/pdf" {
let name_docs = message.name_doc!.split{$0 == "."}.map(String.init)
msg_last = name_docs.last!
cell.img_small_width.constant=28
cell.img_small.image = UIImage(named: "doc")
}else if mimType == nil{
msg_last = message.text!
cell.img_small_width.constant=0
}
DispatchQueue.main.async {
cell.lbl_lastmsg.text = msg_last
}
}
}
})
}
New method:
func GetUnReadMsgCount(groupID : String, cell : GroupsTableViewCell) {
rootRef2.child("group_message_unread").child(current_user!).child(groupID).observe(.childAdded, with: { (snapshot) -> Void in
print(snapshot.childrenCount)
print(snapshot.children.description)
if snapshot.childrenCount > 0 {
DispatchQueue.main.async {
cell.view_count.isHidden = false
cell.lbl_count.text = String(snapshot.childrenCount)
// self.tbl_groups.reloadRows(at: [IndexPath(row: 3, section: 0)], with: .fade)
// self.tbl_groups.reloadData()
}
}
})
}
That's the expected behavior. In a .observe( .value listener, the snapshot is always the complete data at the path. So when you add a child node, the snapshot fires with the entire new data at userRef2.
You have two main options to not get duplicates in your UI:
Clear all messages when there is an update, before adding the data to the table again. So that'd be right before for snap in snapshot.children in your code. This is by far the simplest way to solve the problem if duplicate rows, but may lead to some flashing in your UI.
Listen for child events, instead of observing the entire value. With this you'll get a single .childAdded event for the new child, you can get rid of the for snap in snapshot.children.allObjects and just add the singular new child node to the table.
I am currently getting index out of range but this only happens after the first 3 items have been displayed successfully. I looked through the code and I still don't know where im going wrong. Index out of range happens only when trying to load text in the cells.
Log when method is called from viewdidload:
Description of indexPath: 0
Description of indexPath: 1
Description of indexPath: 2
Log when I press a button to load the rest of images:
Description of indexPath: 0
Description of indexPath: 1
Description of indexPath: 2
Description of indexPath: 3
Datasource method where indexpath is out of range:
//MARK: - tableview datasource methods
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
if arrayOfImgObj.count > 0 {
cell.imageView?.image = arrayOfImgObj[indexPath.row].imageTempt
print("Description of indexPath: ",indexPath.row)
cell.textLabel?.text = arrayOfUIDs[indexPath.row] // index out of range
} else {
print("\(arrayOfImgObj.count) not over 0 yet")
}
return cell
}
Method that is called when viewDidload:
//testing array
var arrayOfUIDs = User().users
func fetchAllUserFristImage() {
Database.database().reference().child("Posts").observe(.childAdded, with: {(snapshot) in
if (snapshot.value as? [String: AnyObject]) != nil {
let user = User()
user.id = snapshot.key
//testing array
self.arrayOfUIDs.append(snapshot.key)
print("\(String(describing: user.id)) <-- SHUD BE THE USERUID")
self.databaseRef = Database.database().reference()
let usersPostRef2 = self.databaseRef.child("Posts").child(user.id!)
usersPostRef2.observe(.value, with: {(postXSnapshots) in
if let postDictionary2 = postXSnapshots.value as? NSDictionary {
for (p) in postDictionary2 {
let posts = p.value as! NSDictionary
//to get back to where i was delete the below for i
for (i) in posts {
let imageUrlString = i.value as! NSDictionary
guard let postUrl = imageUrlString.value(forKey: "image1") else {return}
//below is ting from below
if postUrl != nil {
self.feedArray.append(Post(fetchedImageURL: postUrl as! String))
let imageUrlString = "\(postUrl)"
let imageUrl = URL(string: imageUrlString)!
print("\(imageUrl) this shud be img url's of posts")
let imageDataL = try! Data(contentsOf: imageUrl)
self.imageObject.img2 = UIImage(data: imageDataL)
let image1ToDisplay: UIImage = self.imageObject.img2!
self.arrayOfImgObj.append(forTable(imageTempt: image1ToDisplay))
} else {print("this user had no posts, was nil")}
}
}
}
})
}
//below shud stay same
DispatchQueue.main.async {
self.tableView.reloadData()
}
})
}
Edit:
I have changed teh code to only use one array of objects:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
if arrayOfImgObj.count > 0 {
cell.imageView?.image = arrayOfImgObj[indexPath.row].imageTempt
print("Description of indexPath: ",indexPath.row)
cell.textLabel?.text = arrayOfImgObj[indexPath.row].users[indexPath.row] // index out of range
} else {
print("\(arrayOfImgObj.count) not over 0 yet")
}
return cell
}
changing one line in the function to this:
self.arrayOfImgObj.append(forTable(imageTempt: image1ToDisplay, users: snapshot.key))
Thanks in advance!
Your problem is that you have two arrays:
arrayOfUIDs which has one entry for each user
arrayOfImgObj which has 0-n entries for each user (So, it is likely that the number of objects in this array will be greater than the number of objects in arrayOfUIDs.
You are basing your row count on the arrayOfImgObj, but then using the row number to index into arrayOfUIDs, which results in an array bounds exception.
It may well be more elegant to have table view section per userid, but if you want to put all of the rows in a single section I would suggest you use a single array of structs as your data model.
Use something like:
struct UserImage {
var userID: String
var image: UIImage
}
var tableData = [UserImage]()
Then as you fetch each image, create a new struct and put it in your array:
func fetchAllUserFristImage() {
Database.database().reference().child("Posts").observe(.childAdded, with: {(snapshot) in
if snapshot.value as? [String: AnyObject] != nil {
let user = snapshot.key
self.databaseRef = Database.database().reference()
let usersPostRef2 = self.databaseRef.child("Posts").child(user)
usersPostRef2.observe(.value, with: {(postXSnapshots) in
if let postDictionary2 = postXSnapshots.value as? [String:AnyObject] {
for (p) in postDictionary2 {
if let posts = p.value as? [String:AnyObject] {
//to get back to where i was delete the below for i
for (i) in posts {
if let imageUrlString = i.value as? [String:AnyObject], let postUrl = imageUrlString.["image1"] as? String {
self.feedArray.append(Post(fetchedImageURL: postUrl))
if let imageUrl = URL(string: postUrl), let imageDataL = try? Data(contentsOf: imageUrl), let image = UIImage(data: imageDataL) {
self.tableData.append(UserImage(userID: user, image: image))
} else {print("this user had no posts, was nil")}
}
}
}
}
}
})
//below shud stay same
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
})
}
Now, your cellForRow can be:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
cell.imageView?.image = tableData[indexPath.row].image
cell.textLabel?.text = tableData[indexPath.row].userID
return cell
}
I have made app that save users message and its username to Firebase with childByAutoID key. Inside there are childs that saves Username, message, likes and PostID (as you can see on image below).
After a lot of research and a lot of trying writing code by myself, I figure out that likes need to be saved as autoID keys inside separate child and then you have to count that keys to get number of likes (you'll see that child also on image under and that child is named "Liked")
Everything is displayed in tableView cell.
But all of them are displayed randomly which is OK (I would prefer to be ordered by date added), but what would really like is that loaded data in next next VC to be displayed as:
TOP 10 of the week
TOP 10 of the month
ALL BEST etc...
There'll be separate button for menu and when you press it, you'll be presented with next VC that contain Table View with same data, but this time sorted by most liked post.
This is code that writes keys to LIKED child path (when like is pressed on already loaded data from Firebase database):
#IBAction func likePressed(_ sender: Any) {
let ref = Database.database().reference()
self.likeButton.isEnabled = false
let key = ref.child("Frusters").childByAutoId().key
ref.child("Frusters").child(self.postID).observeSingleEvent(of: .value, with: { (snapshot) in
let updateLikes = ["Liked/\(key)" : key] as [String : Any]
ref.child("Frusters").child(self.postID).updateChildValues(updateLikes, withCompletionBlock: { (error, reff) in
if error == nil {
ref.child("Frusters").child(self.postID).observeSingleEvent(of: .value, with: { (snap) in
if let properties = snap.value as? [String : AnyObject] {
if let likes = properties["Liked"] as? [String : AnyObject] {
let count = likes.count
self.likeLabel.text = "\(count) Likes"
let update = ["likes" : count]
ref.child("Frusters").child(self.postID).updateChildValues(update)
self.likeButton.isHidden = true
self.unlikeButton.isHidden = false
self.likeButton.isEnabled = true
}
}
})
}
})
})
ref.removeAllObservers()
}
and this is the code that loads data and put it in my table view:
func loadData() {
self.fetchPosts.removeAll()
let ref = Database.database().reference()
ref.child("Frusters").observeSingleEvent(of: .value, with: { (snapshot) in
if let postDict = snapshot.value as? [String:AnyObject] {
for (_,postElement) in postDict {
print(postElement);
let post = Post()
post.username = postElement["Username"] as? String
post.message = postElement["Message"] as? String
post.likes = postElement["likes"] as? Int
post.postID = postElement["postID"] as? String
self.fetchPosts.append(post)
}
}
self.tableView.reloadData()
}) { (error) in
print(error.localizedDescription)
}
ref.removeAllObservers()
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.fetchPosts.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell") as! PostTableViewCell
cell.messageLabel.text = self.fetchPosts[indexPath.row].message
cell.usernameLabel.text = self.fetchPosts[indexPath.row].username
cell.likeLabel.text = "\(self.fetchPosts[indexPath.row].likes!) Likes"
cell.postID = self.fetchPosts[indexPath.row].postID
cell.bckgView.layer.cornerRadius = 0
cell.bckgView.layer.shadowOffset = CGSize(width: 0, height: 1)
cell.bckgView.layer.masksToBounds = false
cell.bckgView.layer.shadowColor = UIColor.black.cgColor
cell.bckgView.layer.shadowOpacity = 0.3
cell.bckgView.layer.shadowRadius = 4
return cell
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return UITableViewAutomaticDimension;
}
Well, problem is that I do not know how to insert inside my new UITableView top 10 post with most likes, to sort them from most liked post to next 9 of them with most likes.
Also, is it possible to sort them most liked this month or this week?
Does this keys that Firebase database makes (by autoID) contain date of created post or do I have to insert new child with date inside and then in code combine "date" child and "liked" child to be presented as top 10 liked post between 1st and last of this month?
Thanks in advance. ;)
1-You don't have to store each like separately, if all you care about is the number. You can just update the number.
#IBAction func likePressed(_ sender: Any) {
let ref = Database.database().reference()
self.likeButton.isEnabled = false
let key = ref.child("Frusters").childByAutoId().key
ref.child("Frusters").child(self.postID).observeSingleEvent(of: .value, with: { (snapshot) in
let counted = snapshot.value as? Int
self.ref.child("Flusters".child(self.postID).child("likes").setValue(counted! + 1)
})
2- Yes, you can sort by likes. you'd need to use the .queryOrdered function. Update the code as follows
func loadData() {
self.fetchPosts.removeAll()
let ref = Database.database().reference()
ref.child("Frusters").queryOrdered(byChild: "likes").observeSingleEvent(of: .value, with: { (snapshot) in
if let postDict = snapshot.value as? [String:AnyObject] {
for (_,postElement) in postDict {
print(postElement);
let post = Post()
post.username = postElement["Username"] as? String
post.message = postElement["Message"] as? String
post.likes = postElement["likes"] as? Int
post.postID = postElement["postID"] as? String
self.fetchPosts.append(post)
}
}
self.tableView.reloadData()
3- To order by top week, month, you'd have to keep track of a timestamp.
I click once and it place a tick , though on the second time clicking it will not remove the tick , same goes for when there is a tick, the first time click the tick will be removed but then once removed and I click again it will not replace the tick?
I can only change the tick to no tick or no tick to tick when i go out of the vc and back in ,
Is there an action/function to call that will reload the ability to action a click with out having to go out of the vc and back in?
Here is my code.
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let ref = FIRDatabase.database().reference()
let uid = FIRAuth.auth()?.currentUser?.uid
let key = ref.child("Users").childByAutoId().key
checkIfFollowing(indexPath: indexPath)
var isFollower = false
ref.child("Users").child(uid!).child("following").queryOrderedByKey().observeSingleEvent(of: .value, with: { snapshot in
if let following = snapshot.value as? [String : AnyObject] {
for (ke, value) in following {
if value as? String == self.users[indexPath.row].userID {
isFollower = true
ref.child("Users").child(uid!).child("following/\(ke)").removeValue()
ref.child("Users").child(self.users[indexPath.row].userID!).child("followers/\(ke)").removeValue()
self.tableView(tableView, cellForRowAt: indexPath).accessoryType = .none
//self.checkIfFollowing(indexPath: indexPath)
}
}
}
// if !isFoller - means isFollower is equal to false
if !isFollower {
let following = ["following/\(key)":self.users[indexPath.row].userID]
let followers = ["followers/\(key)" : uid]
ref.child("Users").child(uid!).updateChildValues(following)
ref.child("Users").child(self.users[indexPath.row].userID!).updateChildValues(followers)
//self.checkIfFollowing(indexPath: indexPath)
self.tableView(tableView, cellForRowAt: indexPath).accessoryType = .checkmark
}
})
ref.removeAllObservers()
}
func checkIfFollowing(indexPath: IndexPath){
let ref = FIRDatabase.database().reference()
let uid = FIRAuth.auth()?.currentUser?.uid
ref.child("Users").child(uid!).child("following").queryOrderedByKey().observeSingleEvent(of: .value, with: { snapshot in
if let following = snapshot.value as? [String : AnyObject] {
for (_, value) in following {
if value as? String == self.users[indexPath.row].userID {
self.tableView.cellForRow(at: indexPath)?.accessoryType = .checkmark
}
}
}
})
ref.removeAllObservers()
}
I have a tableview that lists about 10 cells in order from oldest to newest. When I turn on "editing mode" and delete cells, everything appears to work properly.
But I have created some functions that resort these cells into different orders based on various properties. When I use editing mode in these different sort orders, the wrong cells appear to get deleted.
Here is the code for one of my sort functions.
func sortPokemonListbyAveIV(){
self.pokemonList.sortUsingComparator{
(obj1: AnyObject, obj2: AnyObject) -> NSComparisonResult in
let first = obj1 as! NSDictionary
let second = obj2 as! NSDictionary
var firstIVRange = ""
var secondIVRange = ""
if((first["iv"] as! String).containsString("Sorry")){
firstIVRange = "IV: 1 - 2%"
} else {
firstIVRange = (first["iv"] as! String).stringByReplacingOccurrencesOfString("--", withString: "-")
}
if((second["iv"] as! String).containsString("Sorry")){
secondIVRange = "IV: 1 - 2%"
} else {
secondIVRange = (second["iv"] as! String).stringByReplacingOccurrencesOfString("--", withString: "-")
}
// let firstIVRange = first["iv"] as! String
// let secondIVRange = second["iv"] as! String
let firstAveIV = self.avgIV(firstIVRange)
// print(firstIVRange)
let secondAveIV = self.avgIV(secondIVRange)
let result = firstAveIV - secondAveIV
return (result < 0) ? .OrderedDescending
: (result == 0) ? .OrderedSame : .OrderedAscending
}
}
And here is the code for my delete function.
func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {
if editingStyle == UITableViewCellEditingStyle.Delete {
let data = self.pokemonList[indexPath.row]
self.pokemonList.removeObject(data)
self.images.removeObjectForKey(data["imageId"] as! String)
let idToRemove = self.ids[indexPath.row]
let appDel:AppDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
let context:NSManagedObjectContext = appDel.managedObjectContext
if( objects[data["imageId"] as! String] != nil)
{
context.deleteObject(objects[data["imageId"] as! String]!)
}
do {
try context.save()
} catch _ {
}
self.ids.removeObject(idToRemove)
let ref = FIRDatabase.database().reference()
let userId = FIRAuth.auth()!.currentUser!.uid
let key = ref.child("users")
let b = key.child("\(userId)")
let c = b.child("scan")
let d = c.child(idToRemove as! String)
d.removeValue();
tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: UITableViewRowAnimation.Automatic)
tableView.reloadData()
}
}
I suspect I need to edit the delete function code to take into account the current sort order active in the tableview. I'm not quite sure how to do this though.
You don't need to call tableView.reloadData() after tableView.deleteRowsAtIndexPaths. Since calling reloadData will trigger whole bunch of tableView delegate and datasource method, like cellForRowAtIndexPath, numberOfRowsInSection, etc.