Keep selected cell on heavily reloadRowsAtIndexPaths UITableView - objective-c

In one of the views there is a UITableView which is getting updated rather often.
Tracking the changes are done in a classic way using "reloadRowsAtIndexPaths"
-(void)refreshCells:(NSArray *)changedCells
{
NSLog(#"refreshCells %i",[changedCells count]);
[TableView beginUpdates];
[TableView reloadRowsAtIndexPaths:changedCells withRowAnimation:UITableViewRowAnimationBottom];
[TableView endUpdates];
}
Question: How can I preserve the user's last selected Cell. the cell position may change after each update refreshCells?

You can save the current selection with
NSIndexPath *selectedRow = [self.tableView indexPathForSelectedRow];
before the reload and select it again with
if (selectedRow) {
[self.tableView selectRowAtIndexPath:selectedRow animated:NO scrollPosition:UITableViewScrollPositionNone];
}
after the reload. The cell position does not change unless you call insertRowsAtIndexPaths: or deleteRowsAtIndexPaths:.

Related

Dynamically loading UITableView with new rows

I have searched SO and have yet to come to a definitive answer.
I have an app similar to Facebook and Instagram, where once you get to the bottom of the UITableView it calls a web service and loads another 25 rows of data.
The problem I facing is that on loading the new 25 pieces, I am calling reloadData and this causes unpleasant UI flashing.
What's the proper way of dynamically adding the new data to the UITableView as they scroll without the flashing (aka not calling reloadData?
After you add the new row items to your data source you need to use [tableView beginUpdates]; and [tableView endUpdates]; not reloadData.
NSMutableArray *indexPathsToAdd = [NSMutableArray new];
for (Object *object in currentObjects)
{
if (![newObjects containsObject:object]) {
int row = [currentObjects indexOfObject:object];
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:row inSection:0];
[indexPathsToAdd addObject:indexPath];
}
}
[tableView beginUpdates];
[tableView insertRowsAtIndexPaths:indexPathsToAdd withRowAnimation:UITableViewRowAnimationAutomatic];
[tableView endUpdates];

how to know when a UITableView finishes animating when adding and removing cells

I have a table that when you select an item, it changes some stuff about the object in that cell, and then moves the cell to the bottom, and then reloads the table data.
//update model
...
//move cell to bottom
[tableView beginUpdates];
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationBottom];
[tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:[NSIndexPath indexPathForRow:lastIndex inSection:0]] withRowAnimation:UITableViewRowAnimationTop];
[tableView endUpdates];
//reload cells
[tableView reloadData];
currently, it works, but doesn't wait for the animations to finish. How can I wait for the insert and delete animations to finish before reloading the data? or if there is a better way to do it, that would be nice to know as well.
Simple, instead of removing a cell and then immediately adding a new one, you could use UITableView's moveRowAtIndexPath which will animate the cell from its starting point to its ending point.
[tableView moveRowAtIndexPath:[NSArray arrayWithObject:indexPath] toIndexPath:[NSArray arrayWithObject:[NSIndexPath indexPathForRow:lastIndex inSection:0]]];

Core Data: Automatically select next table row when record deleted

I'm working on a Core Data driven iPad app with a split view controller. Just imagine the iPad Mail app and you'll be on the right track. When I select a record in the Root View Controller, the details display in the DetailViewController.
On the Detail View, I have a delete button. When clicked, it tells its Core Data context to delete the current object. It performs the delete correctly and the row disappears from the RootViewController, as it should.
How can I get the RootViewController to automatically select the row after the row that was deleted so it subsequently displays the details in the detail view? (Or automatically select the previous row if the deleted row was the last row?)
If you use an NSFetchedResultsController to manage the table then you can use it's delegate methods to respond to changes.
Use the controller:didChangeObject:atIndexPath:forChangeType:newIndexPath: delegate method and check for the NSFetchedResultsChangeDelete change type.
You can see if the indexPath matches the currently selected rows in your table and then act upon that.
OK. I think I figured it out. Here's what I did. (Please chime in if you see a better way.)
First, I defined a new NSInteger ivar, lastSelectedRow.
Next, I changed
case NSFetchedResultsChangeDelete: {
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
to
case NSFetchedResultsChangeDelete: {
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
lastSelectedRow = indexPath.row;
id <NSFetchedResultsSectionInfo> sectionInfo = [[self.fetchedResultsController sections] objectAtIndex:0];
if (lastSelectedRow == [sectionInfo numberOfObjects]) {
--lastSelectedRow;
}
break;
}
Then I changed
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller
{
[self.tableView endUpdates];
}
to
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller
{
[self.tableView endUpdates];
if (lastSelectedRow > -1) {
NSIndexPath *newIndexPath = [NSIndexPath indexPathForRow:lastSelectedRow inSection:0];
[self.tableView selectRowAtIndexPath:newIndexPath animated:YES scrollPosition:UITableViewScrollPositionNone];
[self tableView:self.tableView didSelectRowAtIndexPath:newIndexPath];
lastSelectedRow = -1;
}
}
Und voila!!
As an added bonus I can now store the last selected row when my app is terminated, and I can go right back to it next time it launches.

UITableViewController canceling Edit mode animation when calling [table reloadData] inside (void)setEditing

I'm having a little problem:
i made my setEditing method:
- (void)setEditing:(BOOL)editing animated:(BOOL)animate {
[super setEditing:editing animated:animate];
[mainTableView reloadData];
}
i'm using the reloadData to call: cellForRowAtIndexPath, and then, if the table is in edit mode, i'll change the appearance of my cell (hiding some labels, for example);
The problem is when i call [mainTableView reloadData] the Edit animation (the red circle slides from left to right and my cell slides to the right) doesn't exist. If don't call it, everything works ok, but i can't customize my cell, since cellForRowAtIndexPath is not called again.
Any suggestion to make it work ??
Thanks!
Maybe you will try to update your table with [tableView beginUpdates] and [tableView endUpdates]? Do you need to reload all the cells or only some of them?
EDIT: Here's the code for reloading all the cells:
[tableView beginUpdates];
NSMutableArray *updatedPaths = [NSMutableArray array];
for (NSNumber *row in yourArray) {
NSIndexPath *updatedPath = [NSIndexPath indexPathForRow:[row intValue] inSection:0];
[updatedPaths addObject:updatedPath];
}
[tableView reloadRowsAtIndexPaths:updatedPaths withRowAnimation:UITableViewRowAnimationFade];
[tableView endUpdates];
yourArray is NSArray instance where you store your cell.textLabel values or something like that...

Is it possible to refresh a single UITableViewCell in a UITableView?

I have a custom UITableView using UITableViewCells.
Each UITableViewCell has 2 buttons. Clicking these buttons will change an image in a UIImageView within the cell.
Is it possible to refresh each cell separately to display the new image?
Any help is appreciated.
Once you have the indexPath of your cell, you can do something like:
[self.tableView beginUpdates];
[self.tableView reloadRowsAtIndexPaths:[NSArray arrayWithObjects:indexPathOfYourCell, nil] withRowAnimation:UITableViewRowAnimationNone];
[self.tableView endUpdates];
In Xcode 4.6 and higher:
[self.tableView beginUpdates];
[self.tableView reloadRowsAtIndexPaths:#[indexPathOfYourCell] withRowAnimation:UITableViewRowAnimationNone];
[self.tableView endUpdates];
You can set whatever your like as animation effect, of course.
I tried just calling -[UITableView cellForRowAtIndexPath:], but that didn't work. But, the following works for me for example. I alloc and release the NSArray for tight memory management.
- (void)reloadRow0Section0 {
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:0 inSection:0];
NSArray *indexPaths = [[NSArray alloc] initWithObjects:indexPath, nil];
[self.tableView reloadRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationNone];
[indexPaths release];
}
Swift:
func updateCell(path:Int){
let indexPath = NSIndexPath(forRow: path, inSection: 1)
tableView.beginUpdates()
tableView.reloadRowsAtIndexPaths([indexPath], withRowAnimation: UITableViewRowAnimation.Automatic) //try other animations
tableView.endUpdates()
}
reloadRowsAtIndexPaths: is fine, but still will force UITableViewDelegate methods to fire.
The simplest approach I can imagine is:
UITableViewCell* cell = [self.tableView cellForRowAtIndexPath:indexPath];
[self configureCell:cell forIndexPath:indexPath];
It's important to invoke your configureCell: implementation on main thread, as it wont work on non-UI thread (the same story with reloadData/reloadRowsAtIndexPaths:). Sometimes it might be helpful to add:
dispatch_async(dispatch_get_main_queue(), ^
{
[self configureCell:cell forIndexPath:indexPath];
});
It's also worth to avoid work that would be done outside of the currently visible view:
BOOL cellIsVisible = [[self.tableView indexPathsForVisibleRows] indexOfObject:indexPath] != NSNotFound;
if (cellIsVisible)
{
....
}
If you are using custom TableViewCells, the generic
[self.tableView reloadData];
does not effectively answer this question unless you leave the current view and come back. Neither does the first answer.
To successfully reload your first table view cell without switching views, use the following code:
//For iOS 5 and later
- (void)reloadTopCell {
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:0 inSection:0];
NSArray *indexPaths = [[NSArray alloc] initWithObjects:indexPath, nil];
[self.tableView reloadRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationNone];
}
Insert the following refresh method which calls to the above method so you can custom reload only the top cell (or the entire table view if you wish):
- (void)refresh:(UIRefreshControl *)refreshControl {
//call to the method which will perform the function
[self reloadTopCell];
//finish refreshing
[refreshControl endRefreshing];
}
Now that you have that sorted, inside of your viewDidLoad add the following:
//refresh table view
UIRefreshControl *refreshControl = [[UIRefreshControl alloc] init];
[refreshControl addTarget:self action:#selector(refresh:) forControlEvents:UIControlEventValueChanged];
[self.tableView addSubview:refreshControl];
You now have a custom refresh table feature that will reload the top cell. To reload the entire table, add the
[self.tableView reloadData]; to your new refresh method.
If you wish to reload the data every time you switch views, implement the method:
//ensure that it reloads the table view data when switching to this view
- (void) viewWillAppear:(BOOL)animated {
[self.tableView reloadData];
}
Swift 3 :
tableView.beginUpdates()
tableView.reloadRows(at: [indexPath], with: .automatic)
tableView.endUpdates()
Here is a UITableView extension with Swift 5:
import UIKit
extension UITableView
{
func updateRow(row: Int, section: Int = 0)
{
let indexPath = IndexPath(row: row, section: section)
self.beginUpdates()
self.reloadRows(at: [indexPath], with: .automatic)
self.endUpdates()
}
}
Call with
self.tableView.updateRow(row: 1)
Just to update these answers slightly with the new literal syntax in iOS 6--you can use Paths = #[indexPath] for a single object, or Paths = #[indexPath1, indexPath2,...] for multiple objects.
Personally, I've found the literal syntax for arrays and dictionaries to be immensely useful and big time savers. It's just easier to read, for one thing. And it removes the need for a nil at the end of any multi-object list, which has always been a personal bugaboo. We all have our windmills to tilt with, yes? ;-)
Just thought I'd throw this into the mix. Hope it helps.
I need the upgrade cell but I want close the keyboard.
If I use
let indexPath = NSIndexPath(forRow: path, inSection: 1)
tableView.beginUpdates()
tableView.reloadRowsAtIndexPaths([indexPath], withRowAnimation: UITableViewRowAnimation.Automatic) //try other animations
tableView.endUpdates()
the keyboard disappear