How to force UITableViewCell selection using selectRowAtIndexPath? - iphone

I am trying to force selection(highlight) of a UITableViewCell using selectRowAtIndexPath..
For eg,
Lets say i want to always have the first cell in a table view to be highlighted,I apply the following logic in viewDidAppear
[self.tableViewPrompts selectRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0] animated:NO scrollPosition:0];
Works nicely.But when i try to apply the same logic elsewhere in the application the cell is not selected.I even tried deselecting other cells before calling the above method and reloading tableview before and after calling this method but none of the approaches seem to work!
I even used the tableview's delegate to forcibly make a call to the didSelectRowAtIndexPath.Event this does not work.
PS: The selection of the cell does not trigger any action all i am trying to achieve is highlighting the desired cell.
What am i doing wrong here?
Help is greatly appreciated!!

The selection of the cell does not trigger any action all i am trying to achieve is highlighting the desired cell.
Why don't you use the cell's highlighted property?
UITableViewCell *cell = [self cellForRowAtIndexPath: [NSIndexPath indexPathForRow: 0 inSection: 0]];
cell.highlighted = YES;
Cant test it now, but it should work.....
edit
Better yet, of course: change your cellForRowAtIndexPath to do that
-(UITableViewCell) tableView:cellForRowAtIndexPath:
//...
if (indexPath.row == 0 && indexPath.section == 0) cell.highlighted = YES
else cell.highlighted = NO;
//...
return cell;

I'm puzzled why isn't this working in your case. Here is my method I use from several places inside my view controller including viewDidAppear and I see no real difference with what you're doing:
- (void)selectRow1
{
NSIndexPath* idx = [NSIndexPath indexPathForRow:1 inSection:0];
[_table selectRowAtIndexPath:idx animated:NO scrollPosition:UITableViewScrollPositionMiddle];
[self tableView:_table didSelectRowAtIndexPath:idx];
}
_table is my table property variable, and the call to didSelectRowAtIndexPath is made so the action of selection could happen but otherwise the only real difference with your code is the scrollPosition parameter.

Simply use setSelected to highlight cell:
[cell setSelected:YES];
I have created this extension for the whole purpose UITableView-Ext

Related

Unnatural jerk with UITableView whe cell height is changed dynamically

Here is what I want in my app. Shown below are two screenshots of the iPhone app Store:
I basically need a "Read More" feature just like it is used in the app store (See the "Description" section in the two images above). I am assuming that each section here (Description, What's New, Information etc.) is a table view cell. And the text inside is a UILabel or UITextField.
Here is what I have tried so far to add this feature:
NSString *initialText = #"Something which is not a complete text and created just as an example to...";
NSString *finalText = #"Something which is not a complete text and created just as an example to illustrate my problem here with tableviews and cel heights. bla bla bla";
NSInteger isReadMoreTapped = 0;
My cellForRowAtIndexPath function:
// Other cell initialisations
if(isReadMoreTapped==0)
cell.label.text = initialText;
else
cell.label.text = finalText;
return cell;
My heightForRowAtIndexPath function:
// Other cell heights determined dynamically
if(isReadMoreTapped==0){
cell.label.text = initialText;
cellHeight = //Some cell height x which is determined dynamically based on the font, width etc. of the label text
}
else{
cell.label.text = finalText;
cellHeight = //Some height greater than x determined dynamically
}
return cellHeight;
Finally my IBAction readMoreTapped method which is called when the More button is tapped:
isReadMoreTapped = 1;
[self.tableView beginUpdates];
[self.tableView endUpdates];
NSIndexPath* rowToReload = [NSIndexPath indexPathForRow:2 inSection:0]; // I need to reload only the third row, so not reloading the entire table but only the required one
NSArray* rowsToReload = [NSArray arrayWithObjects:rowToReload, nil];
[self.tableView reloadRowsAtIndexPaths:rowsToReload withRowAnimation:UITableViewRowAnimationNone];
After doing all this, I do get the required functionality. The new height of that particular cell is calculated and the new text loaded into it. But there is a very unnatural jerk on the TableView which results in a bad User experience. That is not the case with the app store More button though. There is no unnatural jerk in its case. The TableView remains at its place, only the changed cell has its size increased with the text appearing smoothly.
How can I achieve the smoothness as done in the iPhone app store More button?
Your problem might come from reloading the row. You want to try to configure the cell properties directly. I usually use a dedicated method to configure my cell content so I don't have to reload rows.
- (void)configureCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath {
if(isReadMoreTapped==0)
cell.label.text = initialText;
else
cell.label.text = finalText;
// all other cell configuration goes here
}
this method is called from the cellForRowAtIndexPath method and it will configure the cell
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *CellIdentifier = #"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
[self configureCell:cell forRowAtIndexPath:indexPath];
return cell;
}
and you would call this method directly to avoid reloading:
isReadMoreTapped = 1;
[self.tableView beginUpdates];
[self.tableView endUpdates];
NSIndexPath* rowToReload = [NSIndexPath indexPathForRow:2 inSection:0];
UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:rowToReload];
[self configureCell:cell forRowAtIndexPath:rowToReload];
Please try the following changes to your code, I think it will fix your problem.
no need to set cell.label.text in heightForRowAtIndexPath; Please remove them.
in the readMoreTapped, update table is enough:
isReadMoreTapped = 1;
[self.tableView beginUpdates];
[self.tableView endUpdates];
Either remove the calls to:
[self.tableView beginUpdates];
[self.tableView endUpdates];
Or change to ensure that your reloading code is between them. I would remove them as a single row reload is handled well with the method you use:
[self.tableView reloadRowsAtIndexPaths:rowsToReload withRowAnimation:UITableViewRowAnimationNone];
You just need to specify a row animation like fade.
Okay, I finally solved the problem with the help of Matthias's answer (the accepted answer) and my own optimisations. One thing that definitely should be done is to create a dedicated function like configureCell: forRowAtIndexPath: to directly configure cell properties (see Mathias's answer). Everything remains the same with Matthias's answer except:
Before, I was calculating the heights of each cell everytime the heightForRowAtIndexPath function was called without caching(saving) them anywhere and hence when [self.tableView beginUpdates]; and [self.tableView endUpdates]; were called each cell height was calculated again. Instead, what you have to do is to save these cell heights in an array/dictionary so that whenever the More button is tapped, you calculate the height of only the cell that was changed. For the other cells, just query the array/dictionary and return the saved cell height. This should solve any problems with the tableView scroll after the cell height update. If anyone else still face a similar issue as this, please comment on this post. I would be happy to help

Where to use scrollToRowAtIndexPath in iphone?

I am using [self.messageList scrollToRowAtIndexPath:indexPath atScrollPosition:UITableViewScrollPositionTop animated:NO]; in cellForRowAtIndexPath and it is working fine, when a row is added in table array then it auto scroll upside but the problem is that when I try to see the top rows then it automatically come again last rows. And I am not able to see my all rows. Please let me know where I use this or any other approach to do this.
cellForRowAtIndexPath method is not called for all cell that you have. It is only called for visible cell in your TableView. When You Scroll UITableView then it ask to UITableView that you want to use reusable cell or add to new one. generally we use reusable cell so, it is use 1st no of cell and add at end of cell . it is not create another new cell at the end. so, when you scroll your UITableView it not scroll to end of cell it indexPath is similar to indexPath of first no of cell. so you can not scroll your UITableView at to Down.
use following code may be helpful for you.... :)
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
NSString *CellIdentifier = [NSString stringWithFormat:#"S%1dR%1d",indexPath.section,indexPath.row];
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil)
{
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:CellIdentifier];
[self.tblView scrollToRowAtIndexPath:indexPath atScrollPosition: UITableViewScrollPositionTop animated: YES];
// your code
}
// your code
return cell;
}
If you write this code in your cell for row at index path, then it will get called every time you try to scroll manually coz this message is called to draw your cell and you will b directed to the row you specified. Try checking your indexpath in a IF--Else block and scroll only when a certain action happens.
cellForRowAtIndexPath is a method for creating a cell to view .When you scroll this method is called when a cell is viewed.So each time the method called the [self.messageList scrollToRowAtIndexPath:indexPath method(which is called inside cellForRow) is also called and the table scrolls down to that row!!!

Modifying static cells outside cellForRowAtIndexPath without creating new cells

So I get that its typically frowned upon to modify a cell outside of the cellForRowAtIndexPath but here is what I have:
I have a static table that is used as an index of questions (1-33). Each row has a question on it and a detail disclosure indicator. All of this is manually entered in on the stoyboard.
I have a file that lists each question and some properties such as if the question has been answered.
When this screen loads (viewDidAppear) I want to check if each of these questions have been loaded and if so switch the detail indicator to a checkmark.
Now this works, for the first 5 cells. If I go to a question and come back, then even more cells are checked (even if the questions have not been answered). Is this undefined behavior because I am accessing it outside of cellForRowAtIndexPath?
Here is the code I'm using to access and change the cell:
-(void) viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
if (question1Answered)
{
UITableViewCell *cell1 = [self.tableView cellForRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:3]];
[cell1 setAccessoryType:UITableViewCellAccessoryCheckmark];
}
}
Again, it does work for the first 5 elements, then the rest will not change no matter what I do. Then if I go to a question and return it shows more with it selected. Strange behavior...
EDIT: I just noticed that the above code works but it only updates the cells that are currently on the screen. So if I scroll down, leave and come back all the visible cells will have the check mark. Is there a way to force a refresh of all the cells, even if they aren't visible?
Thanks for any and all help...
-David
This is similar to another question I answered few days ago. See stackoverflow.com/a/11770387/1479411
Use delegate method. Put any code that modifies the cell content and [self.tableView reloadData] in the delegate method after returning from the other view controller.
You should not update cell from viewDidAppear.
Instead you should reload data from viewDidApeear.
-(void) viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
if (question1Answered)
{
//This will call your tableview's delegate for visible cells
[self.tableView reloadData];
}
}
And inside cellForRowAtIndexPath, you should take a decision to assign accessory type.
U should first update your model then update your UI according to the model state.
For example if your model is an array of Question object, and each question has some hasBeenAnswered boolean.
Then the only thing u should do in viewDidAppear is to call [self.tableView reloadData], this will update your table view because cellForRowAtIndexPath will be called and set the cells according to your model state.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"CellIdentifier";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if(cell == nil) {
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease];
}
// configure the cell according to your model state
Question *question = [self.questions objectAtIndex:indexPath.row];
// check if this question has been answered
if (question.hasBeenAnswered) {
// if yes - set a checkmark
[cell setAccessoryType:UITableViewCellAccessoryCheckmark];
}
else {
// if not - set to none (or whatever u want)
[cell setAccessoryType:UITableViewCellAccessoryNone];
}
return cell;
}

How to hide custom check mark in table view

I have a UITableiew that I want to be able to display a custom check mark when the row is selected (tapped), however, if there is a check mark on any of the other rows, it must be hidden. What is the best way to do this? The IF... doesn't seem to work at all, it will not hide and unhide the check mark as I thought it would.
Thanks in advance.
-PaulS.
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
home_2_CustomCell *cell = (home_2_CustomCell*)[tableView cellForRowAtIndexPath:indexPath];
//This will hide the selected row...
cell.imageViewDidSelectRow.hidden = YES;
// if (cell.imageViewDidSelectRow.hidden = NO){
// cell.imageViewDidSelectRow.hidden = YES;
// }
}
1) Maintain a tag in class level as an NSIndexPath variable.
2) Whenever a cell is selected make note of the indexPath and reload the table view.
3) In cellForRowAtIndexPath delegate check for this variable and set marks accordingly.
4) This will not be costly if you have the cell with less information.
You can get the each cell of tableview by this code
UITableViewCell *cell=[product_table cellForRowAtIndexPath:[NSIndexPath indexPathForRow:clickedTag inSection:0]];
make one for loop.
for(int i=0;i<[tabledata count];i++){
cell.imageViewDidSelectRow.hidden = YES;
}
for each cell except the current row which is selected and you got only one image displayed for the current row.

reloadRowsAtIndexPaths:withRowAnimation: crashes my app

I got a strange problem with my UITableView: I use reloadRowsAtIndexPaths:withRowAnimation: to reload some specific rows, but the app crashes with an seemingly unrelated exception: NSInternalInconsistencyException - Attempt to delete more rows than exist in section.
My code looks like follows:
[self.tableView reloadRowsAtIndexPaths:[NSArray arrayWithObject:[NSIndexPath indexPathForRow:0 inSection:0]] withRowAnimation:UITableViewRowAnimationFade];
When I replace that reloadRowsAtIndexPaths:withRowAnimation: message with a simple reloadData, it works perfectly.
Any ideas?
The problem is that you probably changed the number of items of your UITableView's data source. For example, you have added or removed some elements from/to the array or dictionary used in your implementation of the UITableViewDataSource protocol.
In that case, when you call reloadData, your UITableView is completely reloaded including the number of sections and the number of rows.
But when you call reloadRowsAtIndexPaths:withRowAnimation: these parameters are not reloaded. That causes the next problem: when you are trying to reload some cell, the UITableView checks the size of the datasource and sees that it has been changed. That results in a crash. This method can be used only when you want to reload the content view of the cell (for example, label has changed or you want to change its size).
Now if you want to remove/add cells from/to a UITableView you should use next approach:
Inform the UITableView that its size will be changed by calling method beginUpdates.
Inform about inserting new row(s) using method - (void)insertRowsAtIndexPaths:(NSArray *)indexPaths withRowAnimation:(UITableViewRowAnimation)animation.
Inform about removing row(s) using method - (void)deleteRowsAtIndexPaths:(NSArray *)indexPaths withRowAnimation:(UITableViewRowAnimation)animation.
Inform the UITableView that its size has been changed by calling the method endUpdates.
I think the following code might work:
[self.tableView beginUpdates];
[self.tableView reloadRowsAtIndexPaths:[NSArray arrayWithObject:[NSIndexPath indexPathForRow:0 inSection:0]] withRowAnimation:UITableViewRowAnimationFade];
[self.tableView endUpdates];
I had this problem which was being caused by a block calling reloadRowsAtIndexPaths:withRowAnimation: and a parallel thread calling reloadData.
The crash was due to reloadRowsAtIndexPaths:withRowAnimation finding an empty table even though I'd sanity checked numberOfRowsInSection & numberOfSections.
I took the attitude that I don't really care if it causes an exception. A visual corruption I could live with as a user of the App than have the whole app crash out.
Here's my solution to this which I'm happy to share and would welcome constructive criticism. If there's a better solution I'm keen to hear it?
- (void) safeCellUpdate: (NSUInteger) section withRow : (NSUInteger) row {
// It's important to invoke reloadRowsAtIndexPaths implementation on main thread, as it wont work on non-UI thread
dispatch_async(dispatch_get_main_queue(), ^{
NSUInteger lastSection = [self.tableView numberOfSections];
if (lastSection == 0) {
return;
}
lastSection -= 1;
if (section > lastSection) {
return;
}
NSUInteger lastRowNumber = [self.tableView numberOfRowsInSection:section];
if (lastRowNumber == 0) {
return;
}
lastRowNumber -= 1;
if (row > lastRowNumber) {
return;
}
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:row inSection:section];
#try {
if ([[self.tableView indexPathsForVisibleRows] indexOfObject:indexPath] == NSNotFound) {
// Cells not visible can be ignored
return;
}
[self.tableView reloadRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationNone];
}
#catch ( NSException *e ) {
// Don't really care if it doesn't work.
// It's just to refresh the view and if an exception occurs it's most likely that that is what's happening in parallel.
// Nothing needs done
return;
}
});
}
After many try, I found "reloadRowsAtIndexPaths" can be only used in certain places if only change the cell content not insert or delete cells. Not any place can use it, even you wrap it in
[self beginUpdates];
//reloadRowsAtIndexPaths
[self endUpdates];
The places I found that can use it are:
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
- (IBAction) unwindToMealList: (UIStoryboardSegue *) sender
Any try from other places like call it from "viewDidLoad" or "viewDidAppear", either will not take effect (For the cell already loaded I mean, reload will not take effect) or cause exception.
So try to use "reloadRowsAtIndexPaths" only in those places.
You should check cell visibility before reload. Here is Swift 3 code:
let indexPath = IndexPath(row: offset, section: 0)
let isVisible = tableView.indexPathsForVisibleRows?.contains{$0 == indexPath}
if let v = isVisible, v == true {
tableView.reloadRows(at: [indexPath], with: .automatic)
}
I had the same issue. In my case; it was happening only if another view controller pop/pushed over existing table view controller and then[self.tableView reloadRowsAtIndexPaths] function is called.
reloadRowsAtIndexPaths call was hiding/showing different rows in a table view which is having over 30, visually complex, rows. As i try to fix the issue i found that if i slightly scroll the table view app wasn't crashing. Also it wasn't crashing if i don't hide a cell (by returning 0 as height)
To resolve the issue, i simply changed the "(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath" function and returned at least 0.01 as row height.
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
....
return rowModel.height + 0.01; // Add 0.01 to work around the crash issue.
}
it solved the issue for me.
THIS IS OLD. DO NOT USE.
I just bumped into this issue when I was calling reloadRowsAtIndexPaths... in order to change the cell to an editing cell containing a UITextField. The error told me I was deleting all of the rows in the table. To solve the problem, I removed:
[self.tableView beginUpdates];
NSArray *reloadIndexPath = [NSArray arrayWithObject:[NSIndexPath indexPathForRow:count inSection:section]];
[self.tableView reloadRowsAtIndexPaths:reloadIndexPath withRowAnimation:UITableViewRowAnimationFade];
[self.tableView endUpdates];
and replaced it with
[self.tableView reloadData];
The app crashes because you have made some changes to your tableView. Either you have added or deleted some rows to the tableView. Hence when the view controller asks your model controller class for data, there is a mismatch in the indexPaths. Since the indexPaths have changed after modification.
So either you simply remove the call
[self.tableView reloadRowsAtIndexPaths:[NSArray arrayWithObject:[NSIndexPath indexPathForRow:0 inSection:0]] withRowAnimation:UITableViewRowAnimationFade];
or replace it with
[self.tableView reloadData];
Calling reloadData checks your number of sections, number of rows in each section and then reloads the whole thing.
If data count changes completely, then use reloadData else, there is three functions to do it.
When data count changes we use insertRows / deleteRows and when data count still the same use reloadRows.
Important! don't forget call beginUpdates and endUpdates between insertRows/deleteRows/reloadRows calls.