Keep timestamp label updated in UITableView - ios

I have a UIViewController that has a UITableView which presents comments fetched form a live Firebase database.
Every time a new comment arrives, I call
tableView.beginUpdates()
tableView.insertRows(at: [IndexPath(row: self.liveComments.count-1, section: 0)], with: .fade)
tableView.endUpdates()
to insert the latest comment with a fade animation. This works fine.
However, each cell has a label that shows when it was posted, in the form of "seconds, minutes or hours ago". The problem is that when many comments arrive, the age label does not get updated, since the existing cells are not updated, and it looks to the user like the comment ages are wrong.
I've tried calling
tableView.reloadRows(at: self.tableView.indexPathsForVisibleRows ?? [], with: .none)
inside my tableView updated block, but the animation is all messed up, since all of the visible cells seem to get animated in a weird, "jumpy" way.
I've also tried getting all of the visible cells, and calling a method on them to update their timestamp labels manually, but I get a crash when I do this, so I guess it's not recommended:
if let visibleCells = self.tableView.visibleCells as? [LiveCommentTableViewCell] {
visibleCells.forEach { cell in
cell.updateCommentAgeLabel()
}
How can I approach this? I just need to reload all visible cells without an animation, and the last cell with a fade in animation. Thank you!

I would just reload all the data, as long the cellForRowAt sets the timestamp label correctly it should work fine:
// still do your nice animation
tableView.beginUpdates()
tableView.insertRows(at: [IndexPath(row: self.liveComments.count-1, section: 0)], with: .fade)
tableView.endUpdates()
// now just refresh the entire table
tableView.reloadData()
of course you're going to want to make sure that whatever collection feeds the numberOfItemsInSection is also updated before calling reloadData() im assuming you're already doing this as well or you'd be running into a lot of bugs and crashes
make sure that code that edits UI is on the main thread too, obviously.
That being said what does your cell.updateCommentAgeLabel() function look like bc that would work in theory as well unless potentially its not being called on the main thread again or the cast isn't working.
Perhaps try telling the system you want it to do a layout pass:
if let visibleCells = self.tableView.visibleCells as? [LiveCommentTableViewCell] {
visibleCells.forEach { cell in
cell.updateCommentAgeLabel()
cell.layoutIfNeeded() // either this
}
tableView.layoutIfNeeded() // OR this at the end, I dont expect you'll need to do both but not sure if both work

Related

Without reloading Tableview, update the changed index cell values using Observer in iOS Swift

In the first time I collect the array values from API response and displayed it in tableview, again I will get the same response from socket and reload the values in table, But here I don't need to reload entire table, I want update the cell's which value has been changed.
Here Compare the two array's, from which index has changes, just need to update that index row cells only, without reload entire table view.old and new array, CodeSample
But you should be careful if will change some indexPaths in you data stack, if it gonna happen - use tableView.deleteRows or tableView.deleteSections. This updates should be qual in table and in dataStack or crash
let indexPaths = [IndexPath]()
tableView.performBatchUpdates {
self.tableView.reloadRows(at: indexPaths, with: .none)
}
or
tableView.beginUpdates()
tableView.reloadRows(at: indexPaths, with: .none)
tableView.endUpdates()
btw, you can make your indexPaths to update like let indexPaths = tableView.indexPathsForVisibleRows - method's name is speechful, in case if you have socket I suppose it would be helpful since u've got dynamic

Reloading the Tableview section after every second is causing flickering

I am trying to reload my Tableview with new datasource every second, but there is a slight flickering in the reloading which is clearly visible to the User.
My requirement for refreshing the datasource every second is because I get the new servertime from the API after every second.
I have used the following different methods to reload my 3 sections of my tableView, but now seems to give a without flicker experience.
Method 1
UIView.performWithoutAnimation {
UIView.setAnimationsEnabled(false)
weakSelf.tradeTableView.reloadSections([0,1,2], with: .none)
}
Method 2
UIView.setAnimationsEnabled(false)
weakSelf.tradeTableView.beginUpdates()
weakSelf.tradeTableView.reloadSections([0,1,2], with: .none)
weakSelf.tradeTableView.endUpdates()
UIView.setAnimationsEnabled(true)
Both methods are giving a flicker experience.
Is it flickering or jumping?
If the scrollview jumps check these answers Jumpy tableview after reload
This this one
let loc = tradeTableView.contentOffset // this line is added to lock offset of tableview
UIView.performWithoutAnimation {
tradeTableView.beginUpdates()
tradeTableView.reloadData()
tradeTableView.endUpdates()
tradeTableView.layer.removeAllAnimations()
tradeTableView.setContentOffset(loc, animated: false)
}
you can try this
UIView.setAnimationsEnabled(false)
self.tradeTableView.beginUpdates()
self.tradeTableView.endUpdates()
UIView.setAnimationsEnabled(true)

UITableView cellForRowAt before numberOfRowsInSection

So I have a view controller that has a table view. This view controller has a button that when clicked opens another view controller. There is a button on this view controller that will change the data set for the table view and dismiss the view controller.
Problem is when changing the data set and dismissing the view controller it calls cellForRowAt. But because the number of items has the potential to decrease I get an Index out of range error.
After setting some break points I realize this is because after updating and dismissing the view controller cellForRowAt gets called but numberOfRowsInSection doesn't. So the number of rows has updated but that isn't reflected in the table view.
I could do a check in cellForRowAt to see if it's out of range before hand and return an empty cell if that's the case, but that seems terribly inefficient. Although it's might be a good idea regardless, in this case seems like such a band-aid fix.
So how can I solve this in an effective and efficient manner?
There are two solutions to resolve this issue.
once the data set is updated just call the reloadData on your tableView which will reload all the data.
if some data is deleted then use deleteRows(at:with:) method .
for single row deletion
data.remove(at: index)
self.tableView.deleteRows(at: [indexPath], with: .fade)
for multiple rows deletion with insertion
var indexPaths: [IndexPath] = []
for index in indexArray
data.remove(at: index)
indexPaths.append(IndexPath(item: index, section: 0))
}
if #available(iOS 11.0, *) {
self.tableView.performBatchUpdates({
self.tableView.deleteRows(at: indexPaths, with: .fade)
})
} else {
self.tableView.beginUpdates()
self.tableView.deleteRows(at: indexPaths, with: .fade)
self.tableView.endUpdates()
}
If you have multiple rows to be removed just create a array of index path with matching row index and pass it to the delete function.
Edit:
use batch updates only for multiple insert/delete/move operations only. as per the apple docs
UITableView defers any insertions of rows or sections until after it has handled the deletions/insertions of rows or sections. This order is followed regardless how the insertion and deletion method calls are ordered.
You can call tableView.reloadData in viewWillAppear method. So it will again reload full tableview with new data.

Change text of a label in the tableview section header

I have an expandable tableview with custom headers. I need to change a text of a label in the header when the table is expanding. I used below code for this.
func toggleSection(header: SuperHeaderDelegate, section: Int) {
sections[section].expanded = !sections[section].expanded
tableView.beginUpdates()
tableView.reloadSections([section], with: .automatic)
tableView.endUpdates()
if let head = header as? SavingAccountHeaderView {
head.accNoLabel.text = "HIIIIIII"
}
else { print("NOPE") }
}
When I use this code it changes the accNoLabel text to new text and change it back to the old text again.
I have tried the tableView.reloadData() instead of the tableView.reloadSections(), then the code worked fine. The accNoLabel didn't change back to its old text. But I really need to use the tableView.reloadSections() to use the animation.
So could anyone tell me what I'm doing wrong?
Couple of things you should check:
Is your 'expanded' toggle happening at the right place (and time)?
reloadSections() is an asynchronous function. So you can't expect your text to be updated 'after' the updates have been completed.
I would suggest toggling your label's text inside the viewForHeader function, because at this point you can be sure that the reloadSections() method is being called and the section's state has already been updated.

App crashes after updating CoreData model that is being displayed in a UITableView

I have a strange bug that is extremely rare but causes the app to crash. I can't reproduce it but I finally found a crash report documenting this.
(I posted the stack trace below. I used a screenshot, as the quotes function here messed up the formatting. That would be unreadable)
So the problem begins after tapping a button, which calls the method closeButtonTapped.
This method is supposed to fade out a popup-view (called ExtendBitPopupView) and save the text the user entered (details attribute of one of my data models).
That's the closeButtonTapped method:
func closeButtonTapped(tap: UITapGestureRecognizer) {
fadeOut { // fadeOut(completion:) just fades out the UI
if self.infoTextView.text != "Enter details..." {
self.entry.info = self.infoTextView.text
self.appDelegate.saveContext()
}
}
}
So it takes the text the user entered and saves it as entry.info to the database.
Now, a little bit of context: The ExtendBitPopupView is a popup that fades in above a UITableView that displays all entry objects there are in the database. It's using a NSFetchedResultsController to manage the data. The table does not show the entry.info attribute. That is only visible inside the ExtendBitPopupView
According to the stack trace, the app crashes while calling the controllerDidChange method. I guess it calls this method because an entry has changed.
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
switch type {
case .insert:
tableView.insertRows(at: [newIndexPath!], with: .automatic)
case .delete:
tableView.deleteRows(at: [indexPath!], with: .automatic)
case .update: // I guess this case is being used
let cell = tableView.cellForRow(at: indexPath!) as! BitCell
let entry = fetchedResultsController.object(at: indexPath!)
cell.configure(entry: entry)
case .move:
tableView.deleteRows(at: [indexPath!], with: .automatic)
tableView.insertRows(at: [newIndexPath!], with: .automatic)
}
}
Line 224 is mentioned in the crash log. It's this line:
let cell = tableView.cellForRow(at: indexPath!) as! BitCell
I can't figure out why the app could crash at this moment. Also, it does work correctly 99% of the time.
My only observation is that when it happens, I typed in quite a lot of text. But I'm not sure about this, as it only happened like 3-4 times so far.
Does anyone have any ideas? I don't know what I can try and I don't know how to reproduce this bug.
If you need any more code, let me know. I just posted the code that is mentioned in the crash log.
Thanks in advance!
indexPath is the index BEFORE the deletes and inserts are applied; newIndexPath is the index AFTER the deletes and inserts are applied.
For updates you don't care where it was BEFORE the inserts and delete - only after - so use newIndexPath not indexPath. This will fix crashes that can happen when you an update and insert (or update and delete) at the same time.
For move the delegate is saying where it moved from BEFORE the inserts and where it should be inserted AFTER the inserts and deletes. This can be challenging when you have a move and insert (or move and delete). I fixed this by saving all the changes from controller:didChangeObject:atIndexPath:forChangeType:newIndexPath: into three different indexPath arrays: insert, delete and update. When you get a move add an entry for it in both the insert array and in the delete array. In controllerDidChangeContent: sort the delete array descending and the insert array ascending. Then apply the changes - first delete, then insert, then update. This will fix crashes that can happens when you have a move and insert (or move and delete) at the same time.
It is the same principle for sections. Save the sections changes in arrays, and then apply the changes in order: deletes (descending), sectionDelete (descending), sectionInserts (ascending), inserts(ascending), updates (any order). Sections can't move or be updated.

Resources