UITableViewCell loses highlighted selection - ios

I am using [tableview reloadData]; to reload the data in my UItableView, however when I use this I loose my highlight on my UItableVewCell.
I would like to know the best way to reinstate this highlight.
I set a tempIndexPath when the user selects the cell they edit the information then I call reloadData, then inside cellForRowAtIndexPath I use this code to re-highlight the cell however its not working.
if ([tempIndexPath isEqual:indexPath]) {
cell.selected = YES;
}

This code keeps the highlighted selection, and is safe with resorting/inserts/repositioning since it keeps a reference to the underlying object from the model, instead of the index path. It also scrolls to the selection, which is helpful when the updated model causes the selection to be moved out of the current scroll position's frame.
- (void) tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
//Save the selected object at this row for maintaining the highlight later in reloadData
_selectedItem = [_items objectAtIndex:indexPath.row];
}
- (void) reloadData
{
[_itemsTable reloadData];
//Reselect and scroll to selection
int numItems = _items.count;
for (int i = 0; i < numItems; i++) {
NSDictionary *dict = [_numItems objectAtIndex:i];
if (dict == _selectedItem) {
//This is more reliable than calling the indexPathForSelectedRow on the UITableView,
// since the selection is cleared after calling reloadData
NSIndexPath *selectedIndexPath = [NSIndexPath indexPathForRow:i inSection:0];
[_itemsTable scrollToRowAtIndexPath:selectedIndexPath atScrollPosition:UITableViewScrollPositionMiddle animated:NO];
[_itemsTable selectRowAtIndexPath:selectedIndexPath animated:FALSE scrollPosition:UITableViewScrollPositionNone];
break;
}
}
}

Save the selected row, reload your table view's data and select the row again.
NSIndexPath* selectedIndexPath = [tableView indexPathForSelectedRow];
[tableView reloadData];
[tableView selectRowAtIndexPath:selectedIndexPath animated:NO scrollPosition:UITableViewScrollPositionNone];
You should know that this is dangerous, because if new items were added to the table view before the selected row, the selection will not be the correct cell. You should calculate how many rows were inserted before the row and adjust the selectedIndexPath accordingly.

Related

UITableView: Keep selection (single selection) when adding cells at top?

This code will restore a cell selection after refresh the UITableView using reloadData:
NSIndexPath *selectedIndexPath = [self.tableView indexPathForSelectedRow];
[self.tableView reloadData];
[self.tableView selectRowAtIndexPath:selectedIndexPath animated:NO scrollPosition:UITableViewScrollPositionNone];
Of course this solution doesn't works, when I add new cells at the top of my TableView. How i can keep selection, when adding cells at the top?
That really depends of your data source. The most common example for simple cases, if it's an array, then you can "remember" the object every time you select a row (tableView:didSelectRowAtIndexPath:)
myObjecyt = [myArray objectAtIndex:indexPath.row];
, and after [table reloadData];, you select the table index:
NSUInteger index = [myArray indexOfObject:myObject];
if (index != NSNotFound) {
NSINdexPath *selectedIndexPath = [NSIndexPath indexPathForRow:index inSection:0];
[self.tableView selectRowAtIndexPath:selectedIndexPath animated:NO scrollPosition:UITableViewScrollPositionNone];
}
But, as I said, it really depends on lot of factors, this is just an example for the basic setup, where the table is populated with data from myArray, without rearranging and omitting items (and has only one section).
You could simply keep track of the number of cells you are adding at the top and increase the index of the row you are selecting with selectRowAtIndexPath: based on that.
Alternatively, if you have any sort of UI element (UIButton, UILabel etc.) then you can set the tag of that element in didSelectRowAtIndexPath: and then access the cell that has the element with that tag (after reloading) using something like UITableViewCell *selectCell = (UITableViewCell *)[[self.view viewWithTag:100] superview];
As #Frane Poljak said, the user tap on a tableView indicates that the object contained in that row is what interests to the user. You have to keep the object as reference to select the cell that contains it again.
Let's say you have an array of Object, you can do this
enum Value
{
case NotSelected
case Selected(object)
}
private var selectionState : Value = .NotSelected
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
selectionState = Selected(dataSource[indexPath.row])
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
[...]
switch selectionState {
case .Selected(let object as! Object):
if dataSource[indexPath.row] == object { cell.selected = true }
else { cell.selected = false }
default : cell.selected = false
}
}
Try this:
-(void) tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath
{
if([indexPath row] == ((NSIndexPath*)[[tableView indexPathsForVisibleRows] lastObject]).row){
//end of loading
int anyIndex = <any number of cell>
NSIndexPath *anyIndexPath = [[NSIndexPath alloc] indexPathForRow:anyIndex inSection:0];
UITableViewCell *anyCell = [table cellForRowAtIndexPath: firstIndexPath]
anyCell.selectionStyle = UITableViewCellSelectionStyle.Default
}
}
This should work.

Xcode iOS UITableView Insert New Cell Animation

Thanks in advance. I'm dealing with a single UITableView which is divided into different sections. Users can add or delete cells only from specific sections. The information displayed in the UITableView is stored in an array where each object represents a section:
eg: tableItemsArray = ((arrayForSection1), (arrayForSection2), (arrayForSection3))
I have the animation set up to handle the delete as follows:
[tableView deleteRowsAtIndexPaths:#[indexPath]
withRowAnimation:UITableViewRowAnimationRight];
I then programmatically remove the item from the array to remove it from the data model. However I am having difficulty in how to insert the cell in an animated form.
Currently I am running the current pseudo-algorithm:
Check item isn't anywhere in the array
Add item to array into a specific section
sort this section of the array alphabetically
reload the data
Update
When a new item is added to the array it is stored in a string to keep record of the new item. Once the array has been resorted I iterate through the array of cells to find its indexPath, then I delete the string (to prevent recursive insertion animations) and perform the animation, however the code doesn't animate its insertion into the table. I wondered whether that maybe the animation code (as below) wasn't working as I was using [tableView reloadData]?
// For Each Section in the table:
for (NSMutableArray *object in sectionsInTable) {
// For Each Cell in a section (Cells are stored in an array in index 1)
for (NSString *label in [object objectAtIndex:1]) {
if ([label isEqualToString:_insertedObject]) {
// New item
_insertedObject = Nil;
// animate addition
[self.labelListTableView beginUpdates];
[self.labelListTableView insertRowsAtIndexPaths:[NSArray arrayWithObject:[NSIndexPath indexPathForRow:[[object objectAtIndex:1] indexOfObject:label] inSection:[sectionsInTable indexOfObject:object]]] withRowAnimation:UITableViewRowAnimationBottom];
[self.labelListTableView endUpdates];
}
}
}
Any ideas why the animation isn't working?
Thanks
D
Seems that the [tableView reloadData] was preventing the animation.
// Annimate the new Item
// For Each Section in the table:
for (NSMutableArray *object in sectionsInTable) {
// For Each Cell in a section:
for (NSString *cell in [object objectAtIndex:1]) {
if ([cell isEqualToString:_insertedObject]) {
// New item
_insertedObject = Nil;
// animate addition
[tableView beginUpdates];
NSInteger section = [sectionsInTable indexOfObjectIdenticalTo:object];
NSInteger row = [[object objectAtIndex:1] indexOfObjectIdenticalTo:cell];
NSIndexPath *newIndexPath = [NSIndexPath indexPathForRow:row inSection:section];
[tableView insertRowsAtIndexPaths: [NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationBottom ];
[tableView endUpdates];
}
}
}

TableViewCell subtitle won't change unless the cell is tapped

The cells in my table have a subtitle set that will show some extra information loaded from a web server. When the app loads the subtitle will just say "Loading..." and then when the response is received, and parsed the cell is updated.
The problem is, unless I tap on the cell the subtitle will stay at "Loading...". As soon as I tap on it it updates to the correct subtitle.
Here I initialize the cell, and set the temporary subtitle while the http request is performed
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
// Setting the tableviewcell titles
UITableViewCell * cell = [tableView dequeueReusableCellWithIdentifier:#"Cell" forIndexPath:indexPath];
cell.detailTextLabel.text = #"Loading...";
cell.detailTextLabel.enabled = NO;
return cell;
}
I've tried making calling the request method in different places:
willDisplayCell and in cellForRowAtIndexPath.
The method that gets the data from the web server uses an asynchronous NSURLConnection which when a successful response is received I update the cell subtitle text using:
// Map Reduce the array used by the TableView
for (int i = 0; i < [self.routes count]; i++) {
if(cellMatches){
UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:[NSIndexPath indexPathForRow:i inSection:0]];
cell.detailTextLabel.text = #"Data received!";
cell.userInteractionEnabled = YES;
cell.textLabel.enabled = YES;
cell.detailTextLabel.enabled = YES;
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
}
I know that you can reload a specific cell using tableView reloadRowsAtIndexPaths but that doesn't seem to work when I implement this code:
[self.tableView beginUpdates];
// Change cell subtitle
[self.tableView reloadRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationNone];
[self.tableView endUpdates];
I have a timer set up to call the request method every 30 seconds, when that is called it works no problem and updates the subtitle right away without me having to tap it. So I think the problem is that the cell isn't initialized or maybe it's being reinitialized after the web request is made. But I don't call reloadAllData during this method.
What you need to do is update your cellForRowAtIndexPath so it checks for the data. If the data is available, set the subtitle to the data, otherwise show "Loading". This requires that you have some sort of data model that stores the data when it is received.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
UITableViewCell * cell = [tableView dequeueReusableCellWithIdentifier:#"Cell" forIndexPath:indexPath];
BOOL enabled = YES;
NSString *subtitle = ... // get the value from the data model
if (subtitle) {
cell.textLabel.text = ... // whatever value goes here
cell.detailTextLabel.text = subtitle;
cell.detailTextLabel.enabled = YES;
} else {
// There's no value for this row yet - show "Loading"
cell.textLabel.text = ... // whatever value goes here when loading
cell.detailTextLabel.text = #"Loading";
cell.detailTextLabel.enabled = NO;
}
return cell;
}
Be sure you set the same set of cell properties in both halves of the if/else statement as needed.
When you get new data and update your data model, simply call reloadRowsAtIndexPaths: for the proper cell path. The above code will then properly update the cell.
The code you have now to update a cell should be removed since it is not the proper way.
Try this:
[self.tableView reloadSections:[NSIndexSet indexSetWithIndex:0] withRowAnimation:UITableViewRowAnimationAutomatic];
Instead of:
[self.tableView reloadRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationNone];
Alongside with rmaddy's solution, I also needed to add one important thing that I found in a similar question:
dispatch_async(dispatch_get_main_queue(), ^{
[self.tableView reloadData];
});
Fixed the issue completely.

Add new UITableView row with custom text

Using this code
- (IBAction)testAdd:(id)sender
{
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:self.numberOfRows inSection:0];
[self.tableView beginUpdates];
self.numberOfRows++;
[self.tableView insertRowsAtIndexPaths:#[indexPath]withRowAnimation:UITableViewRowAnimationBottom];
[self.tableView endUpdates];
}
I'm able to add a new item to a tableView via an 'add' button on the app. This basically adds an item identical to the item already on the table that preceded it.
For example, I have a tableview with the first row displaying a string "TEST", hitting add adds another row that displays "TEST".
I would like to be able to pass in a custom value for the new row, so hitting add outputs a row with say "NEWTHING".
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"UITableViewCell" forIndexPath:indexPath];
// Configure the cell...
cell.textLabel.text = self.val2;
return cell;
}
My data source is actually another view controller that takes user inputs and sends it to my tabelViewController, with the text for the item as "val2".
What I actually want to achieve is the ability to hit add, go back to the user input view controller, get the new data and send it back to my tableViewController to be displayed
What you're asking, is the kinda stuff that is to be done in -cellForRowAtIndexPath: (most of the times, it depends on the way you have designed your datasource) but if it doesn't matter to you, then you can do:
- (IBAction)testAdd:(id)sender
{
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:self.numberOfRows
inSection:0];
self.numberOfRows++;
[self.tableView beginUpdates];
[self.tableView insertRowsAtIndexPaths:#[indexPath]
withRowAnimation:UITableViewRowAnimationBottom];
[self.tableView endUpdates];
UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:indexPath];
[cell.textLabel setText:#"NEWTHING"];
}
But note that when you scroll far up/down and return to this cell, it will most probably show "TEST" (that's where -cellForRowAtIndexPath: will show it's true purpose)
PS: Include your -cellForRowAtIndexPath: method implementation in the question if you want to proceed further
EDIT:
Your -cellForRowAtIndexPath is too static... in the sense that it simply sets self.val2 to cell.textLabel.
Lets say you start with 10 rows, -cellForRowAtIndexPath will be called 10 times and every time, it will set self.val2 onto the current cell's textLabel.
Now... when you add one row (on a button tap), the -cellForRowAtIndexPath will be called for the 11th cell and the same* text will be set to it.
*this technically happened but we quickly changed the cell's text
Basically, the tableView doesn't know how to differentiate between an existing cell and a new added cell because the datasource itself is not dynamic.
To direct the tableView on how to handle different cells, we need to create a more dynamic datasource.
There are different approaches use but I'd generally do it this way:
- (void)viewDidLoad
{
[super viewDidLoad];
self.val2 = #"TEST";
//declare "NSMutableArray *arrDatasource;" globally
//this will be the soul of the tableView
arrDatasource = [[NSMutableArray alloc] init];
int i_numberOfCells = 10;
//populate beginning cells with default text
for (int i = 0; i < i_numberOfCells; i++) {
NSMutableDictionary *dictionary = [[NSMutableDictionary alloc] init];
[dictionary setObject:self.val2 forKey:#"displayText"];
[arrDatasource addObject:dictionary];
}
}
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
//return number of objects in arrDatasource
return arrDatasource.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"UITableViewCell" forIndexPath:indexPath];
//pick up value for key "displayText" and set it onto the cell's label
[cell.textLabel setText:arrDatasource[indexPath.row][#"displayText"]];
//this will be dynamic in nature because you can modify the contents
//of arrDatasource and simply tell tableView to update appropriately
return cell;
}
-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
//make indexPath of new cell to be created
NSIndexPath *indexPathNEXT = [NSIndexPath indexPathForItem:arrDatasource.count inSection:0];
//add the appropriate contents to a dictionary
NSMutableDictionary *dictionary = [[NSMutableDictionary alloc] init];
[dictionary setObject:#"NEWTHING" forKey:#"displayText"];
//add the dictionary object to the main array which is the datasource
[arrDatasource addObject:dictionary];
//add it to tableView
[self.tableView beginUpdates];
[self.tableView insertRowsAtIndexPaths:#[indexPathNEXT]
withRowAnimation:UITableViewRowAnimationAutomatic];
[self.tableView endUpdates];
//this ends up calling -cellForRowAtIndexPath for the newly created cell
//-cellForRowAtIndexPath shows the text (you put in the dictionary in this method above)
}
PS: -cellForRowAtIndexPath: is called whenever cell updates or refreshes or needs to be displayed and so this method needs to be implemented properly

Animation when inserting a larger row into a UITableView assuming the wrong height

I'm having a problem in animating the addition or removal of a row in a UITableView which has a different height than other rows.
The following gifs demonstrats the issue with rows of the default height (44pts) and an larger row (100pts) being inserted and removed. The one on the left is a screen recording from the simulator (the new cell ending up covering row five is a different issue) and the one on the right is a mockup of what it should do.
In my case, I have a bunch of rows, each 60pts in height. When a button in the cell is tapped, an "edit" cell will slide out from underneath, pushing lower cells down. This edit cell is 180pts high. When I call insertRowsAtIndexPaths:withRowAnimation: or deleteRowsAtIndexPaths:withRowAnimation:, the animation assumes the wrong height of 60pts, instead of the 180pts it should be
This means that in the case of UITableViewRowAnimationTop the new cell appears at -60pts from the position it will end up at, and slides down to its new position; about a third of the animation it should be doing. Meanwhile, the row below animates smoothly from its starting position to 180pts downward, exactly as it should.
Has anyone worked out an actual solution to this? some way to tell the new row what hight it's supposed to be for the animation?
Below is the code I am using to hide and show the edit row. I'm using a TLSwipeForOptionsCell to trigger the edit, but it's easily replicated using for example tableView:didSelectRowAtIndexPath:
-(void)hideEditFields{
[self.tableView beginUpdates];
[self.tableView deleteRowsAtIndexPaths:#[[NSIndexPath indexPathForItem:editFormVisibleForRow+1 inSection:0]] withRowAnimation:UITableViewRowAnimationTop];
editFormVisibleForRow = -1;
[self.tableView endUpdates];
}
-(void)cellDidSelectMore:(TLSwipeForOptionsCell *)cell{
NSIndexPath* indexPath = [self.tableView indexPathForCell:cell];
// do nothing if this is the currently selected row
if(editFormVisibleForRow != indexPath.row){
if(editFormVisibleForRow >= 0){
[self hideEditFields];
// update the index path, as the cell positions (may) have changed
indexPath = [self.tableView indexPathForCell:cell];
}
[self.tableView beginUpdates];
editFormVisibleForRow = indexPath.row;
[self.tableView insertRowsAtIndexPaths:#[
[NSIndexPath indexPathForItem:editFormVisibleForRow+1 inSection:0]
] withRowAnimation:UITableViewRowAnimationTop];
[self.tableView endUpdates];
}
}
-(NSInteger) tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{
return _dataSource.count + (editFormVisibleForRow >= 0 ? 1 : 0);
}
-(CGFloat) tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{
int row = indexPath.row;
if(editFormVisibleForRow >= 0 && row > editFormVisibleForRow && row <= editFormVisibleForRow + 1){
return 180.0f;
}
else return 60.0;
}
Poking around a bit, it seems like this is a common issue with no clear answer. Most of the similar questions I've found here on SO are unanswered or offer workarounds specific to the asker's situation. (examples: Problem with RowAnimation, Custom UITableViewCell height yields improper animation, UITableView animation glitch when deleting and inserting cells with varying heights).
Also, instead of trying to make one triple-sized edit row, I tried making three smaller rows and animating them, but this was not suitable because they all appeared at once. I also tried animating them one after the other but the easing made it look odd, with an obvious 3-step animation occurring, instead of the whole edit view sliding out of view in one motion.
Edit: I've just noticed that if I call reloadRowsAtIndexPaths:withRowAnimation: UITableViewRowAnimationNone for the row above the one I'm trying to animate, it changes the behaviour of the animation; namely the animation assumes the height is 0pts, as demonstrated in the following animation. It's closer to what I want, but still not right, as the animation speed is wrong and it leaves a gap (in my app this means the background
colour pokes through)
The solution is pretty straight forward. You need to insert the cell with a height of 0, then change the height to the expected size and then call beginUpdates and endUpdates.
Here is some pseudo code.
var cellHeight: CGFloat = 0
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
let dynamicHeightIndex = 2
if indexPath.row == dynamicHeightIndex {
return cellHeight
} else {
return tableView.rowHeight
}
}
func insertCell() {
// First update the data source before inserting the row
tableView.insertRows(at: [someIndexPath], with: .none)
cellHeight = 200
tableView.beginUpdates()
tableView.endUpdates()
}
To remove the cell, you'll need to wait until the updates animation completes before removing from the table view.
In iOS 11 you have the func performBatchUpdates(_:completion:) which provides a completion block. For previous versions you can try using the CATransaction completion.
cellHeight = 0
CATransaction.begin()
CATransaction.setCompletionBlock({
self.tableView.deleteRows(at: [someIndexPath], with: .none)
})
tableView.beginUpdates()
tableView.endUpdates()
CATransaction.commit()
This, using didSelectRowAtIndexPath worked for me:
#interface TableController ()
#property (strong,nonatomic) NSArray *theData;
#property (strong,nonatomic) NSIndexPath *pathToEditCell;
#end
#implementation TableController
- (void)viewDidLoad {
[super viewDidLoad];
self.theData = #[#"One",#"Two",#"Three",#"Four",#"Five",#"Six",#"Seven",#"Eight",#"Nine"];
[self.tableView reloadData];
}
-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
return ([indexPath isEqual:self.pathToEditCell])? 100: 44;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return (self.pathToEditCell == nil)? self.theData.count: self.theData.count + 1;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
if ([indexPath isEqual:self.pathToEditCell]) {
RDEditCell *cell = [tableView dequeueReusableCellWithIdentifier:#"EditCell" forIndexPath:indexPath];
return cell;
}else{
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"Cell" forIndexPath:indexPath];
cell.textLabel.text = self.theData[indexPath.row];
return cell;
}
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{
if (self.pathToEditCell == nil) { // first time selecting a row
self.pathToEditCell = [NSIndexPath indexPathForRow:indexPath.row +1 inSection:indexPath.section];
[self.tableView insertRowsAtIndexPaths:#[self.pathToEditCell] withRowAnimation:UITableViewRowAnimationAutomatic];
}else if ([self.pathToEditCell isEqual:indexPath]){ // deletes the edit cell if you click on it
self.pathToEditCell = nil;
[self.tableView deleteRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationFade];
}else{ // close the old edit cell and adds another if you click on another cell while the edit cell is on screen
[self.tableView beginUpdates];
[self.tableView deleteRowsAtIndexPaths:#[self.pathToEditCell] withRowAnimation:UITableViewRowAnimationFade];
self.pathToEditCell = indexPath;
[self.tableView insertRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
[self.tableView endUpdates];
}
}
For the deletions, I like the looks of the "fade" option for the animation, but "top" also was ok.

Resources