UITableView cellForRowAt before numberOfRowsInSection - ios

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.

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

Keep timestamp label updated in UITableView

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

Swift, adding a class array into a variable does not update array content in original class

I'm new to IOS so forgive me for my coding mistakes. I'm facing an issue where I have a tableView Controller with two sections. The first section has a button, when clicked, appends data into an array and deletes it's own row in the first section (i did this as there are extra non related rows in the first section). The number of rows in the second section is based upon array.count.
My issue is that I tried begin/end update, and it still doesn't work. Whenever I run the code below and run the startNewDay function (when the button is clicked), this error occurs:
'attempt to insert row 0 into section 1, but there are only 0 rows in section 1 after the update'
This doesn't make any sense, as I appended the array already before I inserted the new rows. The array was empty before I appended it. Shouldn't there be the same number of rows in the second section as array.count?
Table View Delegate code:
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 2
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if section == 0 {
if dataModel.lists[0].dayHasStarted == false {
return 2
} else {
return 1
}
} else {
if itemDoneCount == dataModel.lists[0].item.count && dataModel.lists[0].doneButtonVisible {
return dataModel.lists[0].item.count + 1
} else {
return dataModel.lists[0].item.count
}
}
}
startNewDay button function when pressed:
#IBAction func startNewDayDidPress(sender: AnyObject) {
dataModel.lists[0].dayHasStarted = true
dataModel.lists[0].startDate = NSDate()
addItemButton.enabled = !addItemButton.enabled
// deleting start new day button
let indexPath = NSIndexPath(forRow: 1, inSection: 0)
let indexPaths = [indexPath]
tableView.beginUpdates()
tableView.deleteRowsAtIndexPaths(indexPaths, withRowAnimation: .Fade)
tableView.endUpdates()
// Inserting new array elements and rows into 2nd section
let ritualsArray = dataModel.lists[0].rituals
var itemsArray = dataModel.lists[0].item
itemsArray.appendContentsOf(ritualsArray)
tableView.beginUpdates()
var insertRitualsArray = [NSIndexPath]()
for item in itemsArray {
let itemIndex = itemsArray.indexOf(item)
let indexPath = NSIndexPath(forRow: itemIndex!, inSection: 1)
insertRitualsArray.append(indexPath)
}
tableView.insertRowsAtIndexPaths(insertRitualsArray, withRowAnimation: .Top)
tableView.endUpdates()
}
SOLVED
The problem of this code is not at all related to the previous title of this thread, which may be misleading to people having the same issue as mine. Hence, I will be changing it. The previous title (for the curious) was :
"tableView.begin/end update not updating number of rows in section"
Just for others who might come across this issue, the issue isn't in the tableView delegate, nor is it in reloading the tableview data. For readability, I placed both dataModel.list[0].item into itemsArray and dataModel.list[0].item into ritualsArray. This apparently updates the itemsArray when appended but not the initial dataModel.list[0].item instead, which caused the second section in the tableView not to load the new number of rows, causing the error when inserting rows into non-existant rows.
Hence instead of:
let ritualsArray = dataModel.lists[0].rituals
var itemsArray = dataModel.lists[0].item
itemsArray.appendContentsOf(ritualsArray)
this solved it:
dataModel.list[0].item += dataModel.list[0].rituals
Hope it helps any beginner like me out there that comes across this issue.
Latest update
I found out recently that an array is of value type, and not reference type. Hence placing an array into a variable makes a copy of that array instead of serving as a placeholder for the original array.
Beginner mistake opps.
The error you are receiving means that the datasource contains a different number of items to however many there would be after inserting or deleting rows. This probably means that the data are not being inserted into your datasource array, or that the data do not match the criteria in the if statements in your numberOfRowsInSection function. To troubleshoot this, you should log the contents of the datasource array after modifying it to check what its contents are. If they are what you are expecting (I.e. The data have been added correctly) then the issue is in the way you are evaluating its contents to establish the number of rows. If the contents are not what you are expecting, then the issue is in the way you are inserting the data into the datasource array.
I had a similar problem after deleting a row. It seems that if
numberOfRowsInSection is not coherent (equal to last value -1) this error appears.
I see that there's a condition in your numberOfRowsInSection, this is perhaps the culprit

How to make an Expandable TableView with Profile Img?

I was trying to create a Slide side out menu using the SWRevealViewController but I could not do the expandable TableView and the profile with image + Hello, Name...
And all I could do was:
Basically how do you make an expandable TableView and the profile.
Are there any examples of how to do this?
From what I can tell, you want an expandable table view, ie: Click a row in the table, and more options will appear.
I've achieved that in the past using the following:
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
if let cell = tableView.cellForRowAtIndexPath(indexPath) as? OuterCell{
if let object = data[indexPath.row] as? OuterObject{
if(object.open == false){
data.insert(InnerObject(content: object.innerContent), atIndex: indexPath.row + 1)
tableView.beginUpdates()
tableView.insertRowsAtIndexPaths([NSIndexPath(forRow: indexPath.row + 1, inSection: 0)], withRowAnimation: .Left)
tableView.endUpdates()
}else{
data.removeAtIndex(indexPath.row + 1)
tableView.beginUpdates()
tableView.deleteRowsAtIndexPaths([NSIndexPath(forRow: indexPath.row + 1, inSection: 0)], withRowAnimation: .Left)
tableView.endUpdates()
}
object.open = !object.open
}
}
}
Basically what is happening here is the dataSource for the TableView is represented by the data array in this example. This is what you connect to all the TableViewDelegate hooks.
Within the data array I am holding two types of objects, a OuterObject and an InnerObject.
The OuterObject contains all the information for the inner objects that it contains, and if you click on it, it will dynamically add its children into the data array.
Example Data (I'm using JSON cause its easy to read and understand, you'll have to represent this in swift)
[
{
name="Ensino",
type="outerObject",
open=true
childObjects=[
{
name="Servico",
type="innerObject"
},
{
name="Avaliacao",
type="innerObject"
}
]
},
{
name="Pesquisa",
type="outerObject",
open=false
childObjects=[]
}
]
So in your case you would need an "Ensino" object, with a children property containing data for: Avaliaco, Servicos, ect...
Outer Object also keeps state as to whether or not it is open or closed, that way if you click on it again, you can know to remove all the inner objects that belong to it.
In this example I have hardcoded the OuterObject to always have at most one child, you would have to modify the code to be smarter as to how many objects are inserted/deleted upon click to be based on how many children objects the outer object you click has.
By calling tableView.beginUpdates(), tableView.beginUpdates(), tableView.insertRowsAtIndexPaths() you are accessing the methods that will allow an animated transition to happen when you update the data.
The key to solving this problem is creating an intelligent data source, and then leveraging the methods that Apple has in UITableView to make it happen.
Let me know if you need more help, this is an advanced TableView topic.
check this sample code dropdown menu
this might give you some idea.

Reload Table view cell with animation (Swift)

Is there a way to reload specific UITableView cells with multiple sections with animations?
I've been using:
self.TheTableView.reloadSections(NSIndexSet(index: 1), withRowAnimation: UITableViewRowAnimation.Right)
This animates all the cells in the section though. The other option is to use:
self.TheTableView.reloadRowsAtIndexPaths(<#indexPaths: [AnyObject]#>,
withRowAnimation: <#UITableViewRowAnimation#>)
EDIT:
After using reloadRowsAtIndexPaths
Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Invalid update: invalid number of rows in section 1. The number of rows contained in an existing section after the update (5) must be equal to the number of rows contained in that section before the update (4), plus or minus the number of rows inserted or deleted from that section (0 inserted, 0 deleted) and plus or minus the number of rows moved into or out of that section (0 moved in, 0 moved out).'
Trying to find a smooth way of reloading a tableview by appending objects into an array.
Calling TheTableView.reloadData() before reloadRowsAtIndexPaths works, but the animation is glitchy. Is there another approach?
Why not just reload the cells directly? The indexPath contains the section and row so just keep track of which section you want to reload and fill the indexes in that array.
var indexPath1 = NSIndexPath(forRow: 1, inSection: 1)
var indexPath2 = NSIndexPath(forRow: 1, inSection: 2)
self.tableView.reloadRowsAtIndexPaths([indexPath1, indexPath2], withRowAnimation: UITableViewRowAnimation.Automatic)
Based on your comment you are looking to change your array and have the tableView animate in the changes. If that's the case you should consider using beginUpdates() and endUpdates() for UITableViews or even an NSFetchedResultsController so it handles all of the update animations cleanly.
self.tableView.beginUpdates()
// Insert or delete rows
self.tableView.endUpdates()
I'd recommend using NSFetchedResultsController if you're using Core Data as it simplifies this entire process. Otherwise you have to handle the changes yourself. In other words, if your array changes you need to manually remove or insert rows in the tableview using beginUpdates() and endUpdates().
If you aren't using Core Data, study this to grasp how it's all handled.
In Swift 3.0
We can reload particular row of particular section in tableview
let indexPath = IndexPath(item: 2, section: 0)
self.tableView.reloadRows(at: [indexPath], with: .automatic)
If you want to reload single section in swift 3.0 you can do this:
tableView.reloadSections(IndexSet(integer: 0), with: .automatic)
the define IndexSet in official document:
Manages a Set of integer values, which are commonly used as an index
type in Cocoa API. The range of valid integer values is
in (0, INT_MAX-1). Anything outside this range is an error.
so IndexSet is Set about index, just putting some Int values in [ ]
// for reload one section
tableView.reloadSections([section], with: .automatic)
// or using like this
tableView.reloadSections(IndexSet(integer: section), with: .automatic)
// for reload multi sections
tableView.reloadSections([1, 2, 3, section], with: .automatic)

Resources