UITableView - get list of UITableViewCells in an indexPath.section - iphone

Is there a way I can iterate over a list of UITableViewCells contained in an indexPath.section?
Thanks

You can use UITableView's cellForRowAtIndexPath: to get a cell at a specific index. But this will only return a cell if it is currently visible in the table.
What are you really trying to do? Why do you need all cells in a section?

It's long ago that this question was asked but this might help other people searching for a solution.
NSInteger mySectionIdentifier = 0;
NSArray *visibleCells = self.tableView.visibleCells;
NSMutableArray *sectionCells = [NSMutableArray arrayWithCapacity:visibleCells.count];
for (UITableViewCell *cell in visibleCells) {
NSIndexPath *cellIndexPath = [self.tableView indexPathForCell:cell];
if (cellIndexPath.section == mySectionIdentifier) {
[sectionCells addObject:cell];
}
}
As you see this only works for visible cells but it would be easy to change the code to retrieve all cells instead of the visible ones.

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

How to get a UITableViewCell's subtitle show how many times that specific cell was tapped?

I have a uitableview that displays the values of an array. I would like to know if there is a way to update the subtitle of its table cells, based on how many times each cell was tapped.
Thank you!
First of all, you'll want to use a NSMutableArray so you can change its contents after instantiation. Here's a basic over view of what I just tried to achieve your intended results:
In your interface
#property (strong, nonatomic) NSMutableArray *tapCountArray;
In your implementation
- (void)viewDidLoad
{
[super viewDidLoad];
self.tapCountArray = [NSMutableArray new];
int numberOfRows = 20;
for (int i = 0; i < numberOfRows; i ++) {
[self.tapCountArray addObject:#(0)];
}
}
Then the important part!
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return self.tapCountArray.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"Cell" forIndexPath:indexPath];
NSString *text = [self.tapCountArray[indexPath.row] stringValue];
[cell.textLabel setText:text];
return cell;
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
[self.tapCountArray replaceObjectAtIndex:indexPath.row withObject:#([self.tapCountArray[indexPath.row] intValue] + 1)];
[self.tableView reloadRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
}
When each cell is tapped the number in its detailTextLabel will be incremented up by one.
You shouldn't create a new array or set, that could lead to problems if the two arrays get out of sync with each other. The way to do it, as you suggested in your comment, is to use dictionaries. The way you said you were doing that is probably not the way, however. You want an array of dictionaries where the values for one key would be whatever your main data is and the value for the other key would be the number of taps. For example, lets call the two keys main and sub, and your main data is a set of names. The array of dictionaries would look like this: ({main:#"Tom",sub:1}, {main:#"Dick", sub:0}, {main:#"Harry",sub:2}, .....). In the tableView:cellForRowAtIndexPath:indexPath method you would provide the data to the cells like this:
cell.textLabel.text = [[array objectAtIndex:indexPath.row] valueForKey:#"main"];
cell.detailTextLabel.text = [[array objectAtIndex:indexPath.row] valueForKey:#"sub"];
I think you can just set up another array of the same length as the one you have now. Then when your didSelectRowAtIndexPath is triggered, increment your indexPath.row entry of the new array and refresh that cell. If you don't expect to shuffle the table, you don't need a dictionary.
You can insert the object into an NSCountedSet, and on your cellForRowAtIndexPath method, you would take the model object for the cell and verify the number of times it has been inserted into the NSCountedSet instance.
Take a look at the NSCountedSet documentation: https://developer.apple.com/library/mac/#documentation/Cocoa/Reference/Foundation/Classes/NSCountedSet_Class/Reference/Reference.html

How to select a UITableView row that is not yet shown?

I'm not sure I'm phrasing my question correctly, so here's the details.
I'm using a UITableView to display the list of available fonts. When the list is dsiplayed,
only about 12 rows show at a time, so if the previously selected font is not yet show, I can't select it when first showing the view.
What I'd like is to have the cell selected and shown in the center of the list when the view appears. But since the UITableView only loads data as needed, this is the best I can get:
EDITED
I've tried this but it doesn't work (the cell is only briefly selected while scrolling):
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"Cell";
UITableViewCell *cell=[tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if(cell==nil){
cell=[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
}
[cell.textLabel setText:[fontArray objectAtIndex:indexPath.row]];
[cell.textLabel setFont:[UIFont fontWithName:[fontArray objectAtIndex:indexPath.row] size:16]];
[cell setSelectionStyle:UITableViewCellSelectionStyleBlue];
//select the cell/row if it matches the current font
if([cell.textLabel.text isEqualToString:currentFontName]){
cell.selected=YES;
}
NSLog(#"returning cell %#",cell.textLabel);
return cell;
}
1 - Make your comparison using - (BOOL)isEqualToString:(NSString *)aString
1a - replace your test
if([cell.textLabel.text isEqualToString:currentFontName]){
cell.selected=YES;
}
by
cell.selected = [cell.textLabel.text isEqualToString:currentFontName];
1b - if you need to display your selected font you can do that before loading your TableView:
NSIndexPath * selFntPath = [NSIndexPath indexPathForRow: [fontArray indexOfObject: currentFontName]
inSection: 0];
[tableView scrollToRowAtIndexPath: selFntPath
atScrollPosition: UITableViewScrollPositionMiddle
animated: NO];
2 - Check that you do not unselect your cell in
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath delegate method.
This is a classic behavior in most sample codes.
option: you can keep the select property for user selection and toggle a specific control (ie checkmark using accessoryType property your cell) to show a system-selected row.
This is probably the right approach but you can't test NSStrings for equality by pointer comparison. You want - (BOOL)isEqualToString:(NSString *)aString instead of ==.
I found the solution in another thread - the problem is related to reusing cells. If I do not re-use cells, then everything works properly. Re-using cells also caused problems with multiple checkmarks appearing when only one item is selected. Thanks to those who contributed.
EDIT: If I should not be answering my own questions please tell me...but also tell me the proper way to resolve the question!
EDIT 2: This thread also helped
UITableViewCell going black when selected programmatically

Referencing a UITableViewCell that gets created in cellForRowAtIndexPath

I have a button on my TableView header that is an empty box, and when pressed, is a checkmark. I have that same button on the cells for that header. So I basically want to perform an action on each cell that is in that section. I'm using the Animating TableView from WWDC 2010 as my example. The header object has an array of integers that keep track of how many rows are in its section.
I'm not really sure from here, how I can get the custom UITableViewCell to perform my action on. Any thoughts? Thanks.
So far I have something like this:
- (void)selectAllCheckmarksInSectionHeaderView:(SectionHeaderView *)sectionHeaderView forSection:(NSInteger)section {
SectionInfo *sectionInfo = [self.SectionInfoArray objectAtIndex:section];
int totalRows = [[sectionInfo rowHeights] count];
for (int row = 0; row < totalRows; row++) {
NSIndexPath *path = [NSIndexPath indexPathForRow:(NSUInteger)row inSection:section];
OrderTableViewCell *cell = (OrderTableViewCell *)[_orderTableView cellForRowAtIndexPath:path];
cell.CheckmarkButton.selected = YES;
}
}
However my OrderTableViewCell is nil.
To reference a button that is a located in a section header, you'll have to assign it to an instance variable to keep a reference to it. To obtain references to all of the cells in a section and update them, you'll want to try something like this...
CGRect sectionRect = [self.tableView rectForSection:0]; // Rect for first section
NSArray *indexPathsInSection = [self.tableView indexPathsForRowsInRect:sectionRect];
for (NSIndexPath *indexPath in indexPathsInSection)
{
// Obtain the cell at each index path and update its accessory state
UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:indexPath];
cell.accessoryType = UITableViewCellAccessoryCheckmark;
}
Of course this is an overly simplistic example. A more flexible approach would be to update some model object when a section is "selected" and reload the table. Then in your -tableView:cellForRowAtIndexPath: you would determine whether a cell in a section should be checkmarked based off the model object for the whole section.

UITableViewCell: how to verify the kind of accessoryType in all cells?

I have a UITableView in that some cells are marked with UITableViewCellAccessoryCheckmark at the initialization of the view.
When the user selects another row, I have to check if the maximum number of selected rows was achieved before. To do that, I used the code bellow:
- (NSInteger)tableView:(UITableView *)tableView numberOfSelectedRowsInSection:(NSInteger)section{
NSInteger numberOfRows = [self tableView:tableView numberOfRowsInSection:section];
NSInteger numberOfSelectedRows = 0;
for (int i = 0; i < numberOfRows; i++) {
UITableViewCell *otherCell = [tableView cellForRowAtIndexPath:[NSIndexPath indexPathForRow:i inSection:section]];
if (otherCell.accessoryType == UITableViewCellAccessoryCheckmark) {
numberOfSelectedRows++;
}
}
return numberOfSelectedRows;
}
If my number of rows is, as example, 20, the variable numberOfRows is setted correctly with 20. Lets say that 13 rows already are marked with UITableViewCellAccessoryCheckmark. So, numberOfSelectedRows should be 13 after the loop, but only the marked and VISIBLE cells are considered. So, if I have 9 cells showed and 7 are marked, the numberOfSelectedRows returns 7 instead of 13 (but the for iterate 20 times, as expected).
Is this a correct behavior of UITableView or it is a bug of iPhone simulator?
Thanks in advance.
Yes, it works as designed. You should never store model data in your views. UITableView knows nothing about the data, it only displays cells (and throws them aways as soon as they scroll off the screen). You need to store the checkmark state of each cell in a model object (e.g. an array) that you then access from your view controller.
This is correct behavior.
The UITableView is not a list. The system caches cell that are off screen to save memory and CPU and they can not be iterated over in a manner that makes sense.
Ok, you should keep track of the model/data and the tableView will keep track of displaying it. I have had some problems with this until I accepted that uitableView is not a list:)
So, have an array of objects that each corresponds to the data in the a cell. When building the individual cells like this:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *CellIdentifier = #"categoryCell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:CellIdentifier] autorelease];
}
Item *item = [self.itemList objectAtIndex:indexPath.row];
[cell.textLabel setText:[item itemBrand]]; //notice that here we set the cell values
return cell;
}
The when a user clicks you change you model like this:
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
NSLog(#"IndexPat.row%i", indexPath.row);
Item item = (Item*) [self.itemList objectAtIndex:indexPath.row];
//change the state of item
}
This way the tableView will update to resemble the model/data, you just managed the model.