36

I write custom jabber client in iphone.

I use xmppframework as engine.

And I have UITableViewController with NSMutableArray for repesent contact list.

When i receive(or somebody change it contents) roster (aka contact list) i wanna change UITableView items (add/remove/modify). So if User work with listView at time when list updates by

[items addObject:newItem];
[self.tableView reloadData];

user lost current selection item.

So, my question is howto save (if possible, i mean if given selected item not removed) current select item after reloadData?

Thx.

vinnitu
  • 4,234
  • 10
  • 41
  • 59

14 Answers14

69

The easy way is something like this:

NSIndexPath *ipath = [self.tableView indexPathForSelectedRow];
[self.tableView reloadData];
[self.tableView selectRowAtIndexPath:ipath animated:NO scrollPosition:UITableViewScrollPositionNone];
Marco
  • 845
  • 7
  • 5
  • [cell setSelected:YES] doesn't work correctly. So I used this methode which works fine – Vassily Jun 14 '12 at 10:15
  • Found you should not put this in a [self.tableView beginUpdates]/[self.tableView endUpdates]. – ChrisP Aug 15 '12 at 01:23
  • 11
    isn't working in iOS8; apparently the reloadData method hasn't finished before selectRowAtIndexPath fires. It works if you add a bit of a delay, but then the row selection flickers... – ecume des jours Sep 29 '14 at 02:16
  • 2
    is there a way to fix it but a bit of a delay in ios8? – signal Oct 29 '14 at 08:46
  • The `dispatch_after` example provided by @adam-wallner has the delay part. My only problem is how long do I need to delay it to make it save enough even if the device is quite busy. I've been delaying 0.15 seconds and this bug is still occurred. – Firanto Jun 25 '15 at 08:24
26

Adding a bit to Marco's answer:

First, if you're using multiple selection, you can add this method to your ViewController and call it whenever you need to call [tableView reloadData]:

- (void)reloadTableView
{
    NSArray *indexPaths = [self.tableView indexPathsForSelectedRows];
    [self.tableView reloadData];
    for (NSIndexPath *path in indexPaths) {
        [self.tableView selectRowAtIndexPath:path animated:NO scrollPosition:UITableViewScrollPositionNone];
    }
}

Second, if you're in a UITableViewController and you want to preserve selection after tableView appears there's a feature in UITableViewController: clearsSelectionOnViewWillAppear you can turn on or off.

See here: http://developer.apple.com/library/ios/#documentation/uikit/reference/UITableViewController_Class/Reference/Reference.html

marmor
  • 27,641
  • 11
  • 107
  • 150
12

Swift 4.2 Tested

The correct way to update selected rows after reload table view is:

let selectedRows = tableView.indexPathsForSelectedRows

tableView.reloadData()

DispatchQueue.main.async {
    selectedRows?.forEach { selectedRow in
        tableView.selectRow(at: selectedRow, animated: false, scrollPosition: .none)
    }
}
nsinvocation
  • 7,559
  • 3
  • 41
  • 46
muhasturk
  • 2,534
  • 20
  • 16
  • Rather than doing it like this, I used `setNeedsLayout()` on the selected cell: `tableView.cellForRow(at: selectedRow)?.setNeedsLayout()` – huwr May 28 '19 at 05:32
5

The workaround is to use reloadSections: instead of reloadData. For some reason reloadData removes the current selection.

Greg
  • 71
  • 1
  • 5
  • 5
    I have to correct myself. It works perfectly in iOS 4.0 but not on 4.2 and higher –  May 11 '11 at 08:54
4

Adding a delay didn't work for me (tested on iOS 8.4 and iOS 9). What did work was adding a call to -layoutIfNeeded on the selected cell, after calling -selectRowAtIndexPath:animated:scrollPosition:.

NSIndexPath *selectedIndexPath = [self.tableView indexPathForSelectedRow];
[self.tableView reloadData];
[self.tableView selectRowAtIndexPath:selectedIndexPath animated:NO scrollPosition:UITableViewScrollPositionNone];
[[self.tableView cellForRowAtIndexPath:selectedIndexPath] layoutIfNeeded];
antonjn
  • 138
  • 1
  • 3
  • A delay smells unreliable, this worked for me. If you are doing it from `viewWillAppear`, perform the re-selection before calling `super`. – Sebastian May 26 '16 at 11:37
3

On iOS 9.3 and Swift 2.x, I simply had to call the function on the main thread :)

self.tableView?.selectRowAtIndexPath(indexPath, animated: false, scrollPosition: .None)
Kevin Delord
  • 2,498
  • 24
  • 22
1

This works for me:

NSIndexPath *indexPath = [table indexPathForSelectedRow];
[table reloadData];
double delayInSeconds = 0.01;
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, delayInSeconds * NSEC_PER_SEC);
dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
    [self.table selectRowAtIndexPath:indexPath animated:YES
                              scrollPosition:UITableViewScrollPositionNone];
});

The trick is to make selectRowAtIndexpath run later a bit. The code above is an Xcode template you can select when you start writing dispatch_after.

Adam Wallner
  • 2,292
  • 23
  • 20
  • Any idea how long do we need to delay it? In my case using iOS 8, on simulator, this bug never occurred, I suspect because simulator was way much faster than the actual device. But when I test it on iPhone, I set the delay to 0.15 second and I still get the exception. – Firanto Jun 25 '15 at 08:20
1

This is a solution for the case if you want to do table updates and keep the selection:

    NSIndexPath* pathToSelect = [self.tableView indexPathForSelectedRow];
    if (pathToSelect && newRows) {
        int row = pathToSelect.row;
        for (NSIndexPath* path in newRows) {
            if (path.section == pathToSelect.section && path.row <= pathToSelect.row) {
                row++;
            }
        }
        pathToSelect = [NSIndexPath indexPathForRow:row inSection:pathToSelect.section];
    }

    [self.tableView beginUpdates];
    if (reloadRows) [self.tableView reloadRowsAtIndexPaths:reloadRows withRowAnimation:UITableViewRowAnimationFade];
    if (newSections) [self.tableView insertSections:newSections withRowAnimation:UITableViewRowAnimationFade];
    if (newRows) [self.tableView insertRowsAtIndexPaths:newRows withRowAnimation:UITableViewRowAnimationFade];
    [self.tableView endUpdates];

    if (pathToSelect) {
        [self.tableView selectRowAtIndexPath:pathToSelect animated:NO scrollPosition:UITableViewScrollPositionNone];
    }
Ezeki
  • 1,519
  • 10
  • 17
1

Edit:

After testing, it doesn't always work!,

the reason is the index may change !

so the correct way is
make new variable

var currentIndex : IndexPath?

and on didSelectRow

    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {

        currentIndex = indexPath
}

then change the function to

 func reloadTableView() {
        let indexPaths = tableview.indexPathsForSelectedRows
        if let currentIndex = self.currentIndex {
        self.tableview.reloadRows(at: [currentIndex], with: .fade)
        for path in indexPaths ?? [] {
            tableview.selectRow(at: path, animated: false, scrollPosition: .none)
            }}
    }

==========

@marmor Answer worked

this is a Swift version of his code

Swift 4.2.3

func reloadTableView() {
        let indexPaths = tableView.indexPathsForSelectedRows
        tableView.reloadData()
        for path in indexPaths ?? [] {
            tableView.selectRow(at: path, animated: false, scrollPosition: .none)
        }
    }
Basel
  • 550
  • 8
  • 21
1

A lot of these answers are basically using hacks to re-select the row that was previously highlighted by finding out what the previously selected row was from the table view which seems like a bad way to go about it, because usually when your tableview reloads, there is a change in the data that is populating the tableview.

So rather base you re-selection of the row based on your data

let data = ["some", "array", "of", "data"]
let selected = "of" // this should be populated from didSelectRowAt indexPath:

tableView.reloadData()

if let index = data.firstIndex(of: selected) {
  tableView.selectRow(at: IndexPath(row: index, section: 0), animated: false, scrollPosition: .none) // the "of" row should now be highlighted, regardless if the data changed order
}
Fonix
  • 11,447
  • 3
  • 45
  • 74
0

SWIFT 3:

self.collectionView.reloadData()
self.collectionView.selectItem(at: indexPath, animated: false, scrollPosition: [])
Yaroslav Dukal
  • 3,894
  • 29
  • 36
0

I solved it better by declaring an array to store, let's say, the ID or String Text of the cell you have selected, and adding it on the didSelectItemAtIndexPath function. This way, even if the number of rows changes, the selected ones won't. For example:

var selectedItems: [String] = [] // This array type really depends on your preference and what property you're using to store
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
    guard let cell = collectionView.cellForItem(at: indexPath) else { return }
    selectedItems.append(cell.textField.text)
}

And on your initial cellForItemAtIndexPath

func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
    let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "CollectionCell", for: indexPath)
    if let text = cell.textField.text {
        if selectedItems.contains(text) {
            collectionView.selectItem(at: indexPath, animated: true, scrollPosition: [])
        }
    }
    return cell
}
tapizquent
  • 668
  • 11
  • 24
0

my two cents for UICollectionView: (credits to Basil..)

I do run in main thread as I reload from a background task:

final override func safeReloadData(){

            DispatchQueue.main.async { [weak self] in

                guard let self = self else {
                    return
                }

                let indexPaths = self.collectionView.indexPathsForSelectedItems
                self.collectionView.reloadData()
                for path in indexPaths ?? [] {
                    self.collectionView.selectItem(at: path, animated: false, scrollPosition: [])
                }
            }
    } 
ingconti
  • 10,876
  • 3
  • 61
  • 48
-5

It sounds like you are not using a 'model' for the data - rather simply updating the 'view' (user interface), and thus is probably a bad design.

reloadData should cause the view to be updated with data from the model, which should contain the most current data to be displayed.

search for resources on the 'model view controller pattern'

Mobs
  • 1,572
  • 12
  • 17
  • no, I really use NSMuttableArray for delegate Table Data Source and add/remove data in this array, but to make refresh tableView I call reloadData method after modifying array. Am I right? But after this tableView lost selection. – vinnitu Dec 02 '09 at 08:05
  • Its hard to help you without seeing your code, but the tableView's methods should create the table from the data source, thus you simply update the data model and fire reloadData. – Mobs Dec 02 '09 at 08:41