Strange tableView row/section animation when row is selected - ios

This cool expanding tableView works great but when you select some of the rows, a weird animation occurs.
The table has multiple sections, and each row can show additional rows when clicked. Row appearance and behavior is structured in a plist using various values e.g. if a row's boolean "isExpandable" property in the plist is set to true and its "additionalRows" property is set to 2, the row can expand to show 2 sub-rows.
The issue with the table is that when the first cell in each section is clicked, the animation runs fine, but when other cells are clicked, they shuffle:-
I suspect the strange animation happens because all sections are reloaded when a cell is clicked because of this line under didSelectRowAtIndexPath in the ViewController file:-
tbl.reloadSections(NSIndexSet(index: indexPath.section), withRowAnimation: .Automatic)
I've tried the following but they either crash the app or cause the cells to not expand:-
Setting the animation method to .None
Using reloadData instead
Storing the indexPaths of the selectedRow and its sub-rows, and then reloading all with reloadIndexPaths
Storing the rows of the selected cell and its subcells then reloading them once when a row is clicked
Someone on the actual site that the code is posted on suggested using insertRowsAtIndexPath and deleteRowsAtIndexPath instead of reloadSection, but I'm not sure how to do it. Your help is greatly appreciated.
EDIT
- Also tried insertRowsAtIndexPath and deleteRowsAtIndexPath but it seems to conflict with the way the tableView model is set up and the app crashes.

Inserting rows into the table should help you with more consistent table view animation behaviour.
To insert rows at specific index paths you can use the 'insertRowsAtIndexPaths' method. You will need to generate each index path that you are inserting and then pass them to the method in an array.
ObjC:
NSArray* arr = your NSIndex Paths...;
[tableView insertRowsAtIndexPaths:arr withRowAnimation:UITableViewRowAnimationFade];
Swift:
table.insertRowsAtIndexPaths([IndexPaths..], withRowAnimation: .Automatic)
https://developer.apple.com/library/ios/documentation/UIKit/Reference/UITableView_Class/#//apple_ref/occ/instm/UITableView/insertRowsAtIndexPaths:withRowAnimation:

Here is the problem: reloadSections(..) makes the tableView conveniently fetch all of the newest data from the model (for the specified section(s)) at the cost of more 'vague' animation. Use insertRowsAtIndexPaths(..) with the .Top animation style for the behavior you are looking for. If you have issues with your Data Model, fix that first (because it will almost certainly bother you later).

Related

How can the content of a UITableViewCell be updated properly?

I have a TableView whose cells content need to be updated when it's TableViewRowAction is excecuted. More precisely I want to update a UILabel inside the cell. Setting a new text to this label inside the action is no problem but when scrolling down/up and the cell gets reloaded, then this label inside the cell has the same text as before changing it.
I know that the cells get reused and that's why I want to know how to "avoid" this effect and how to update the content of the cells properly. I have already tried to call TableView.ReloadData() which seems to solve the problem but the cells appear in a completly different order which doesn't look very nice.
The most straightforward approach is to update the data that your UITableView is drawing from (i.e. the array of data that you're populating each cell from has been updated to reflect your text change), then reload the specific cell of the UITableView:
let indexPath = IndexPath(row: 0, section: 0)
tableView.reloadRows(at: [indexPath], with: .automatic)
As you said table cells are reused , this means you have to keep a model array for the whole table and make all the changes to it , then reload the table more importantly to set the content of the cell index in cellForRowAt
To achieve this you must create an Array that acts as a dataSource for your labels. When you are changing the text for a specific label, you must update the array. You can get the exact update location from the property indexPath of the method cellForRowAt. Every single time you scroll up and down, cellForRowAt method is called. Just use something like
label.text = array[indexPath.row]

deleteRowsAtIndexPaths resets custom cells

I'm relatively new to Swift and iOS and I have one issue - I have a custom SwipeCell class that extends UITableViewCell. It's a cell that can be swiped left and right, and has buttons on each side. For some reason if I made the buttons out of the frame of the cell when user swipes the cell they would appear but could not call an action, so my solution was to make cell wider than it is (so buttons can fit in it). Because it was done this way my cell has to have an offset by default for the total width of the buttons of the left (let's say 100), so it's position is X:-100.
And that's fine, and everything works fines with the cells, however there is one huge issue - if I call the deletion of any cell from the tableView like this
tableView.deleteRowsAtIndexPaths([tableView.indexPathForCell(activeCell)!], withRowAnimation: .None)
tableView then deletes the cell, and all of the other cells that are currently visible (doesn't happen with cells above or below the screen bounds) get moved to X:0 instead of staying at X:-100, so I assume that deleteRowsAtIndexPaths calls some function that resets the visible cells positions to 0,0. I'm currently setting swipe cells positions with layoutSubviews() since the number of buttons is dynamic and couldn't be determined upfront, but layoutSubviews is not the function where the bug happens.
So to sum up question - what function does deleteRowsAtIndexPaths call after deleting a cell that resets/redraws the visible cells?
deleteRowsAtIndexPaths deletes a row(s) from the tableView. This is handled internally by iOS. From Apple documentation:
Deletes the rows specified by an array of index paths, with an option
to animate the deletion.
Another interesting thing to note here is that this method does not modify your model (object that holds data used by your table view cells to render on themselves). You have to do that yourself. The cells are deleted, but if you call reloadData without deleting the row from your Model, cell will reappear.
Expect that cells get deleted and created all the time. Cells are very, very temporary objects. Write the code to create one when needed, and don't make any assumptions. Don't assume the cell is there later, don't assume it's in the same row, don't assume it displays the same data (because cells are recycled).
Since I could find out what happens inside of deleteRowAtIndexPaths, and why it changes frames of each Cell, I decided to just do my own function that deletes a row, by obtaining cells with tableView.visibleCells, and then just moving cells bellow the deleted cell up by a height of deleted cell. Thank you all for trying to help me, and especially thanks to Abhinav who told me that it was handled internally, which help me decide to write a custom code for the deletion.

How can I reload only updated cells from UICollectionView

I have several cells that are written onto a UICollectionView and reloading them takes a long time. How can I reload only when the data of a cell is modified? Here's one thing I've tried:
if([oldDataSource count] != [currentDataSource count])
{
[collectionView reloadData];
}
Yet, I still don't want to deal with reloading the entire thing. I've also tried:
if([oldDataSource count] < [currentdataSource count])
{
//something was added. //reload the last item in the current data source
}
This also failed because I can't reload something that doesn't "exist" in memory yet. This method also doesn't work because if something was deleted from my data source, the Cell will still remain in the interface, and will crash if clicked on.
So, what's the best way to deal with edited cells?
Thanks!
Reloads just the items at the specified index paths.
- (void)reloadItemsAtIndexPaths:(NSArray *)indexPaths
Discussion
Call this method to selectively reload only the specified items. This causes the collection view to discard any cells associated with those items and redisplay them.
try and get the cell u want to change. then:-
If u can get it. change the data source and reload it.
if it doesnt reflect the changes. change the data source, then the repective new values of the cell, then call setNeedsDisplay on the cell.
If the cell is not returned (that means) it is out of the view. Simply change the data source. The change in the data source will be reflected when the cell is brought to view
try any of these three.
cheers. have fun

TableView reloadData vs. beginUpdates & endUpdates

I got a tricky problem regarding updating my TableView, i get different results using different methods of updating it, let me explain:
Situation 1:
I use [tbl reloadData]; where tbl is my TableView, to update the TableView - works as intended.
Situation 2:
I use:
[tbl beginUpdates];
[tbl reloadRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationRight];
[tbl endUpdates];
Where tbl is my TableView, and indexPaths is an array containing all the indexPaths present in the TableView. Now the array is fine, it contains all the correct indexPaths (double and triple checked) but for some reason - this does not work as intended.
Now I realize that this is an X-Y problem (where I ask for Y but my problem is really X because I think solving Y will solve X) and thats only because I feel it's a bit complicated explaining X (the consequence of said above problem) in an easy way, so I'd rather refrain from that if possible.
So, down to my question: Is there a difference between the two ways of updating the TableView (aside from the animation bit of course) or should I suspect the problem to lay elsewhere?
EDIT:
Okay, I'll try to explain what the symptoms are:
In the cellForRowAtIndexPath-method I add a button to each cell with an assigned tag which is equal to the cell's indexPath row, like such:
btn.tag = indexPath.row;
The reason I do this is so I can identify each button as they all call the same function:
- (void)btnPressed:(id)sender
When I then update the cells - because some values in the cells have changed - Situation 1 makes everything work fine, Situation 2 however - mixes up the tags so the next time one of the buttons are pressed, they no longer have the correct tags.
The mix-up does appear random to me, but the randomization occurs differently depending on which cells button I press first. I hope this clarifies my problem.
From the UITableView documentation
beginUpdates
Begin a series of method calls that insert, delete, or
select rows and sections of the receiver.
That means, you should not use this unless you are inserting, deleting or selecting. You are doing neither of these.
Also, you should end beginUpdates with endUpdates, not reloadData. Documentation:
This group of methods must conclude with an invocation of endUpdates.
The first difference between reloadData and reloadRowsAtIndexPaths is that there are 2 UITableViewCell objects allocated simulteaneosuly for the same indexPath when doing reloadRowsAtIndexPaths (because the tableview 'blends' in the the new cell) . This is sometimes not foreseen by the code in cellForRowAtIndexPath .The surprise comes from the fact that even if a cell was already allocated for a particular cell identfier the table view does not give you back this cell in dequeueReusableCellWithIdentifier when calling reloadRowsAtIndexPaths, instead it returns nil. In contradiction reloadData reuses the cells it already allocated .
The 2nd difference is that endUpdates after reloadRowsAtIndexPaths directly calls cellForRowAtIndexPath (if you set a breakpoint there,endUpdates is visible in the stack trace) whereas reloadData schedules the calls to cellForRowAtIndexPath at a later time (not visible in the stack trace).
However you would need to post a bit more code to give us insight what you are doing there. In principle the indexPaths of the new cells are identical to the old ones also with reloadRowsAtIndexPaths as long as you don't delete or insert rows.
Call this method if you want subsequent insertions, deletion, and selection operations (for example, cellForRowAtIndexPath: and indexPathsForVisibleRows) to be animated simultaneously.
I think this is what you want. beginUpdates & endUpdates can change the UItableview with animation.

Graphical glitches when adding cells and scrolling with UITableView

I am using a UITableView to display the results of a series of calculations. When the user hits 'calculate', I add the latest result to the screen. When I add a new cell, the UITableViewCell object is added to an array (which is indexed by tableView:cellForRowAtIndexPath:), and then I use the following code to add this new row to what is displayed on the screen:
[thisView beginUpdates];
[thisView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation: UITableViewRowAnimationFade];
[thisView endUpdates];
This results in the new cell being displayed. However, I then want to immediately scroll the screen down so that the new cell is the lowermost cell on-screen. I use the following code:
[thisView scrollToRowAtIndexPath:newIndexPath atScrollPosition:UITableViewScrollPositionBottom animated:YES];
This almost works great. However, the first time a cell is added and scrolled to, it appears onscreen only briefly before vanishing. The view scrolls down to the correct place, but the cell is not there. Scrolling the view by hand until this invisible new cell's position is offscreen, then back again, causes the cell to appear - after which it behaves normally. This only happens the first time a cell is added; subsequent cells don't have this problem. It also happens regardless of the combination of scrollToRowAtIndexPath and insertRowsAtIndexPath animation settings.
EDIT:
I've now started inserting cells at the second-to-last position of the table, rather than the end, and the problem still occurs - when first inserted, a cell is 'invisible' until it goes offscreen and comes back on again. What could be causing this, and how can I force the cell to be drawn as soon as it is added to the table?
You're having problems because your updating the table without updating the data model backing it. Tables don't actually know how many rows they have nor what cells to display. They depend on the datasource and the delegate to tell them these things. Your design expects the table itself to track them.
insertRowsAtIndexPaths: is intended to be used for moving existing rows around a table, not for adding entirely new logical rows. When you insert an entirely new cell, the tableview looses track of how many rows it actually has.
Before you display a new row, the first thing you should do is update the values returned by:
– numberOfSectionsInTableView:
– tableView:numberOfRowsInSection:
... to reflect the addition of the new rows. This will allow the table to understand how big it is.
Then you need to update cellForRowAtIndexPath: to return the correct cell for the added row. Then you need to reload the table.
After you've done that, you should be able to scroll the tableview to the end and have the cell display properly.
The important thing to remember about tables is that they are dumb. The table itself holds no data, doesn't know how many sections and rows it has or what order the rows and sections come in. All the logic about data, sections, rows, cells and cell contents comes from the datasource and/or the delegate. When you want to change a table, you actually change the datasource and/or the delegate and then the table will reflect those changes automatically.
Edit:
Upon rereading the parent, I see that your putting the actual UITableViewCell objects in your data array and that you have one cell for each row.
This is not how tableviews are supposed to work and this will not scale beyond a few dozen rows at most.
Tableviews are intended to be an illusion that allows you display a lOGICAL table which has an arbitrary high number or rows. To that end, it only keeps enough UITableViewCell objects alive to cover the visually displayed area in the UI. With a default cell height of 44 pixels this means a tableview will never have more than 9 cell objects at a time.
Instead of eating memory holding cells that are not displayed, the tableview lets the delegate dequeue a cell that has scrolled off screen, repopulate it with the data of another LOGICAL row and then display it in a new position. This is done in cellForRowAtIndexPath:
You really need to start over here with your design. Your data needs to be kept separate from the user interface objects. You don't want to have more cells alive at anyone time than absolutely necessary because your memory use will balloon and your response time will degrade. Your current problem is the result of this unusual design.
When you've done that, you can add the result row as outlined above.
Try to scroll with some time shift after cell update via NSTimer or performSelector:withDelay:. It can help but to fix all problems I think there need to do more work.
The glitches may be caused because a UITableView considers itself the owner of any UITableViewCell instances it is displaying, and reuses them as needed. Part of that process is calling prepareForReuse on the cell. Since you are keeping the cells in an array, you do not want them reused. Try implementing an empty prepareForReuse in your UITableViewCell class. Or just create cells dynamically in tableView:cellForRowAtIndexPath: as apple recommends.
I used what Skie suggested to avoid the problem in the following way:
Immediately after adding the row:
[self performSelector:#selector(scrollToDesiredArea:) withObject:newIndexPath afterDelay:0.4f];
This called the following:
-(void)scrollToDesiredArea:(NSIndexPath *)indexPath{
UITableView *thisView = (UITableView*)self.view;
[thisView scrollToRowAtIndexPath:indexPath atScrollPosition:UITableViewScrollPositionBottom animated:YES];
}
The delay of 0.4s seems to be sufficient to avoid the glitching; any less and it still happens. It may have to be different on varying models of iPhone hardware, though - I only tested on emulator.

Resources