I have a table view with 10 items. I can click a button and their order changes.
Then I reload this with the following code (I have and will ever have only one section):
[self.tableView reloadSections:[NSIndexSet indexSetWithIndex:0] withRowAnimation:UITableViewRowAnimationAutomatic]
I tried different animations, but nothing works as I want.
I want the rows to slide into their new positions. How do I do this?
What I did:
[tableView beginUpdates]
Save all current cells to NSMutableArray. The model object is also stored in custom cell property.
Change data source.
Iterate over cells in array (index i), for each cell iterate over data source array (index j). If object in cell and in data source is the same, move cell from position i to position of object in data source array (j).
[tableView endUpdates]
You can animate the transitions with UITableView methods beginUpdates and endUpdates, in conjunction with moveRowAtIndexPath.
Here's a sample of sorting animation with asc/desc
table.beginUpdates()
for (index,_) in tableviewItems.enumerated(){
let newRow = tableviewItems.count - index - 1
table.moveRow(at: IndexPath(row: index, section: 0), to: IndexPath(row: newRow, section: 0))
}
tableviewItems.reverse()
table.endUpdates()
Related
How can one insert rows into a UITableView at the very top without causing the rest of the cells to be pushed down - so the scroll position does not appear to change at all?
Just like Facebook and Twitter when new posts are delivered, they are inserted at the top but the scroll position remains fixed.
My question is similar to this this question. What makes my question unique from that question is that I'm not using a table with fixed row heights - I'm using UITableViewAutomaticDimension and an estimatedRowHeight. Therefore the answers suggested there will not work because I cannot determine the row height.
I have tried this solution that doesn't involve taking row height into consideration, but the contentSize is still not correct after reloading, because the contentOffset set isn't the same relative position - the cells are still pushed down past where they were before the insert. This is because the cell hasn't been rendered on screen so iOS doesn't bother to calculate the appropriate height for it until it's about to appear, therefore contentSize is not accurate.
CGSize beforeContentSize = tableView.contentSize;
[tableView reloadData];
CGSize afterContentSize = tableView.contentSize;
CGPoint afterContentOffset = tableView.contentOffset;
tableView.contentOffset = CGPointMake(afterContentOffset.x, afterContentOffset.y + afterContentSize.height - beforeContentSize.height);
Alain pointed out rectForCellAtIndexPath which forces iOS to calculate the appropriate height. I can now determine the proper height for the inserted cells, but the scroll view's contentSize is still not correct, as is evidenced when I iterate over all cells and add up the heights which is a larger than contentSize.height. So ultimately when I set the contentOffset manually it's not scrolling to the correct location.
Original code:
//update data source
[tableView beginUpdates];
[tableView insertRowsAtIndexPaths:newIndexPaths withRowAnimation:UITableViewRowAnimationAutomatic];
[tableView endUpdates];
In this scenario, what can be done to achieve the desired behavior?
Late to the party but this works even when cell have dynamic heights (a.k.a. UITableViewAutomaticDimension), no need to iterate over cells to calculate their size, but works only when items are added at the very beginning of the tableView and there is no header, with a little bit of math it's probably possible to adapt this to every situation:
func tableView(tableView: UITableView, willDisplayCell cell: UITableViewCell, forRowAtIndexPath indexPath: NSIndexPath) {
if indexPath.row == 0 {
self.getMoreMessages()
}
}
private func getMoreMessages(){
var initialOffset = self.tableView.contentOffset.y
self.tableView.reloadData()
//#numberOfCellsAdded: number of items added at top of the table
self.tableView.scrollToRowAtIndexPath(NSIndexPath(forRow: numberOfCellsAdded, inSection: 0), atScrollPosition: .Top, animated: false)
self.tableView.contentOffset.y += initialOffset
}
Also, single row sections can accomplish desired effect
If a section already exists at the specified index location, it is moved down one index location.
https://developer.apple.com/library/ios/documentation/UIKit/Reference/UITableView_Class/index.html#//apple_ref/occ/instm/UITableView/insertSections:withRowAnimation:
let idxSet = NSIndexSet(index: 0)
self.verseTable.insertSections(idxSet, withRowAnimation: .Fade)
I think your solution was almost there. You should have based the new Y offset on the "before" Y offset rather than the "after".
..., beforeContentOffset.y + afterContentSize.height - beforeContentSize.height
(you would need to save the beforeContentOffset point before the reload data though)
Meet the same problem and still do not find a elegant way to solve it.Here is my way,suppose I want to insert moreData.count cells above the current cell.
// insert the new data to self.data
for (NSInteger i = moreData.count-1; i >= 0; i--) {
[self.data insertObject:moreData[i] atIndex:0];
}
//reload data or insert row at indexPaths
[self.tableView reloadData];
//after 0.1s invoke scrollToRowAtIndexPath:atScrollPosition:animated:
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[self.tableView scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:moreData.count inSection:0] atScrollPosition:UITableViewScrollPositionTop animated:NO];
});
The advantage is fixed the position before and after inserting new data.The disadvantage is we can see the screen flash (the topmost cell shows and move up)
My working solution for iOS 13 & Swift 5:
Note: Only tested with dynamic cell heights (UITableViewAutomaticDimension / UITableView. automaticDimension).
P.S. None of the solutions posted here did work for me.
extension UITableView {
/**
Method to use whenever new items should be inserted at the top of the table view.
The table view maintains its scroll position using this method.
- warning: Make sure your data model contains the correct count of items before invoking this method.
- parameter itemCount: The count of items that should be added at the top of the table view.
- note: Works with `UITableViewAutomaticDimension`.
- links: https://bluelemonbits.com/2018/08/26/inserting-cells-at-the-top-of-a-uitableview-with-no-scrolling/
*/
func insertItemsAtTopWithFixedPosition(_ itemCount: Int) {
layoutIfNeeded() // makes sure layout is set correctly.
var initialContentOffSet = contentOffset.y
// If offset is less than 0 due to refresh up gesture, assume 0.
if initialContentOffSet < 0 {
initialContentOffSet = 0
}
// Reload, scroll and set offset:
reloadData()
scrollToRow(
at: IndexPath(row: itemCount, section: 0),
at: .top,
animated: false)
contentOffset.y += initialContentOffSet
}
}
I have a UITableView and in its -tableView:didSelectRowAtIndexPath: method I want to change the appearance of two cells (the one that has just been selected and the one that was selected before). I do that with this method:
// reload both rows to change appearance
if (![indexPath isEqual:activeIndexPath]) {
[self.tableView reloadRowsAtIndexPaths:[NSArray arrayWithObjects:activeIndexPath, indexPath, nil]
withRowAnimation:UITableViewRowAnimationAutomatic];
}
With the parameter UITableViewRowAnimationAutomatic set it works great: the corresponding rows are updated with a smooth transition. But I want those two rows to update immediately without an animation.
I thought that UITableViewRowAnimationNone would do the trick but if I use that constant for the row animation the cells are not being updated at all
I have a table view with selectable rows.
When I reload the table view some new rows might be added (or removed) and some labels in the table view's cells might change. That's what I want to achieve by calling [tableView reloadData].
Unfortunately that method also clears the table view's whole state - including the selection. But I need to keep the selection.
So how can I reload all the data in a table view while still keeping the selected rows selected?
You can store the index path of the selected row with:
rowToSelect = [yourTableView indexPathForSelectedRow];
Before reload the data. And after reload use:
[yourTableView selectRowAtIndexPath:rowToSelect animated:YES scrollPosition:UITableViewScrollPositionNone];
JeroVallis solution works for single selection table views.
Based on his idea this is how I made it work with multiple selection:
NSArray *selectedIndexPaths = [self.tableView indexPathsForSelectedRows];
[tableView reloadData];
for (int i = 0; i < [selectedIndexPaths count]; i++) {
[tableView selectRowAtIndexPath:selectedIndexPaths[i] animated:NO scrollPosition:UITableViewScrollPositionNone];
}
The most efficient way is to keep the selected state in the data model
Add a boolean property isSelected in the struct or class which represents the data source.
In cellForRowAt set the selected state of the cell according to the property.
In didSelectRow toggle isSelected in the data source item and reload only the particular row at the given index path.
An alternative that has some advantages is to only reload the rows that have not been selected. Swift code below.
if var visibleRows = tableView.indexPathsForVisibleRows,
let indexPathIndex = visibleRows.index(of: indexPath) {
visibleRows.remove(at: indexPathIndex)
tableView.reloadRows(at: visibleRows, with: .none)
}
Swift 4.2 Tested
The correct way to update selected rows after reload table view is:
// Saves selected rows
let selectredRows = tableView.indexPathsForSelectedRows
tableView.reloadData()
// Select row after table view finished reload data on the main thread
DispatchQueue.main.async {
selectredRows?.forEach({ (selectedRow) in
tableView.selectRow(at: selectedRow, animated: false, scrollPosition: .none)
})
}
After adding a row to a table, I'm using scrollToRowAtIndexPath to position the row in the middle of the screen, then relying on scrollViewDidEndScrollingAnimation to highlight the same row.
Sometimes, the row can't be scrolled; e.g., when it's one of the first in the table. In these cases, scrollViewDidEndScrollingAnimation isn't called so the newly added row isn't highlighted.
Is there a way to know whether or not scrollToRowAtIndexPath will have any effect on the table?
While inserting you would have to specify indexPaths to be inserted right?
Store these indexPaths publically and reload that row with highligted color in CellForRowAtIndexPath or reload the rows individually.
Eg:
Add the indexpaths of inserted row to the public array.
[tableView insertRowsAtIndexPaths:indexpathArray withRowAnimation:UITableViewRowAnimationFade];
self.colorIndexs = indexpathArray;
Then add this in cellForRowAtIndexPath
if ([colorIndexs count] > 0 && [colorIndexs containsObject:indexPath])
customCell.contentView.backgroundColor = [UIColor redColor];
Ok, I'm stuck. This is an extension of a previous post of mine. Here is what I am trying to do.
I have an Edit button on a navigation bar that when pressed adds a cell at the beginning of my one section table view. The purpose of this cell if to allow the use to add new data to the table; thus it's editing style is Insert. The remaining cells in the table are configured with an editing style of Delete.
Here is my setediting method:
- (IBAction) setEditing:(BOOL)isEditing animated:(BOOL)isAnimated
{
[super setEditing:isEditing animated:isAnimated];
// We have to pass this to tableView to put it into editing mode.
[self.tableView setEditing:isEditing animated:isAnimated];
// When editing is begun, we are adding and "add..." cell at row 0.
// When editing is complete, we need to remove the "add..." cell at row 0.
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:0 inSection:0];
NSArray* path = [NSArray arrayWithObject:indexPath];
// fill paths of insertion rows here
[self.tableView beginUpdates];
if( isEditing )
[self.tableView insertRowsAtIndexPaths:path withRowAnimation:UITableViewRowAnimationBottom];
else
[self.tableView deleteRowsAtIndexPaths:path withRowAnimation:UITableViewRowAnimationBottom];
[self.tableView endUpdates];
// We nee to reload the table so that the existing table items will be properly
// indexed with the addition/removal of the the "add..." cell
[self.tableView reloadData];
}
I am accounting for this extra cell in my code, except I now have two index paths = [0,0] - the new cell and the old original first cell in the table. If I add a call to reload the table view cell in setEditing, the cells are re-indexed, but now my table view is no longer animated.
I want my cake and eat it too. Is there another way to accomplish what I am trying to do and maintain animation?
--John
You can do what you want but you need to keep your data source consistent with the table. In other words, When the table is reloaded, tableView:cellForRowAtIndexPath and the other UITableViewDataSource and UITableViewDelegate methods responsible for building the table should return the same cells depending on editing state that you are adding/removing in setEditing:antimated:.
So, when you insert/delete a cell in setEditing:animated: you need to also make sure your data source reflects the same change. This can be tricky if you are adding a special cell to the beginning of a section but the rest of the data is from an array. One way to do this is while reloading the table, if editing, make row 0 the add cell and use row-1 for your array index for subsequent cells. If you do that you'd also need to add one to tableView:numberOfRowsInSection: to account for the extra cell.
Another way would be to have a section for the add cell and it would have 0 rows when not editing, 1 row otherwise and you return the appropriate cell. This will also require you to configure your table and cell(s) appropriate depending on how you want things to look.