Updating UITableViewCell dynamically - iphone

How can one change the textLabel or the detailTextLabel of a specific UITableViewCell dynamically?
Basically, I am downloading a set of files from the internet whenever the user taps on a cell. Now instead of the progress indicator, I would like to display the name of the file currently being downloaded. I would like to keep updating the detailTextLabel to show the (current) filenames until all the files are downloaded.

Assuming you're in a method like tableView:didSelectRowAtIndexPath: (or have another way to get the index path to the cell), you can do something like this:
UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:indexPath];
UILabel *label = nil;
for(UIView *subview in cell.contentView.subviews) {
if([subview isKindOf:[UILabel class]]) {
label = (UILabel*)subview;
break;
}
}
if(label)
label.text = #"New text";
This will find the first UILabel in the cell, and may not work if your cell has more than one label. A more robust way to do it is to set the tag of the label when you create the cell (this can be done in Interface Builder or in code). Then you can find the label like this:
UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:indexPath];
UILabel *label = nil;
for(UIView *subview in cell.contentView.subviews) {
if([subview isKindOf:[UILabel class]] && [subview tag] == kDetailLabelTag) {
label = (UILabel*)subview;
break;
}
}
if(label)
label.text = #"New text";

Just in case anyone still has this problem I have had success with calls to the reloadData method. For example in my code in the viewWillAppear method, I have the line:
[tableSettings reloadData];
Where tableSettings is the name of my UITableView.

Related

How do I add a UIActivity Indicator to every Cell and maintain control of each individual indicator

I'm trying to add an activity indicator to certain cells in my UITableView. I do this successfully in the method didSelectRowAtIndexpath using
CGRect CellFrame = CGRectMake(260, 10, 20, 20);
actindicator = [[UIActivityIndicatorView alloc]initWithFrame:CellFrame];
[actindicator setHidesWhenStopped:NO];
[actindicator setActivityIndicatorViewStyle:UIActivityIndicatorViewStyleGray];
actindicator.tag =1;
[[cell contentView] addSubview:actindicator];
The catch is I need to control these multiple activity indicators from ANOTHER METHOD. I figured a property was a way to do this however by initialising a new instance of actIndicator every time, I loose reference's to all but the 'latest' init of the activity indicator thus meaning I can only control one.
What do i need to do here (if even possible?) to maintain reference to all the actIndicators so i can begin animating ALL of them?
Or Can I somehow use the actindicator.tag to control some form of reference.
Many thanks for any help.
EDIT: (Derived from answer) to access all Instances of Activity indicator with a tag of 1 in tableView (visible cells only) can use below from another method:
for (UITableViewCell *cell in [self.tableView visibleCells]) {
UIActivityIndicatorView *actView = (UIActivityIndicatorView *)[cell.contentView
viewWithTag:1];
[actView startAnimating];
activityFlag = 1;
}
The method above will cycle through all visible cells and start animating the activity indicator.
To handle the case of the tableview being scrolled, I re-animate the indicators using the method below which is in cellForRowAtIndexPath. cellStateKey simply indicates if the cell has a checkmark next to it, if it does have a checkmark and my activityflag (async webserver call in progress)is set..then i want to continue animating.(technically re-start animation, as scrolling tableview stops it)
if ([[rowData objectForKey:cellStateKey] boolValue]) {
cell.accessoryType = UITableViewCellAccessoryCheckmark;
if(cell.accessoryType == UITableViewCellAccessoryCheckmark &&activityFlag ==1){
for (UIView *sub in [[cell contentView] subviews]) {
if (sub.tag == 1) {
UIActivityIndicatorView *acView = (UIActivityIndicatorView *)
[cell.contentView viewWithTag:1];
[acView startAnimating];
}
}
As mentioned in origional question I initialise my activity indicators (and remove activity indicators aswell if required) in the method didSelectRowAtIndexPath
You can add UIActivityIndicatorView as cell's accessoryView.
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
UIActivityIndicatorView *spinner = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray];
spinner.frame = CGRectMake(0, 0, 24, 24);
UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath];
cell.accessoryView = spinner;
[spinner startAnimating];
[spinner release];
}
A simple way to do this (assuming you're adding the indicator as in your code) is to first get a collection of the visible cells (or rows) in your table, by calling [tableView visibleCells]. Then iterate through the cells like this:
for (UITableViewCell *cell in [tableView visibleCells]) {
for (UIView *sub in [cell subViews]) {
if (sub.tag == 1) { // "1" is not a good choice for a tag here
UIActivityIndicatorView *act = (UIActivityIndicatorView *)sub;
[act startAnimating]; // or whatever the command to start animating is
break;
}
}
}
There's more complexity for you to deal with: in your original code, you need to make sure you're not adding an additional activity indicator to a pre-existing cell each time cellForRowAtIndexPath is called, and you need to account for the situation where the user might scroll the table at a later point, exposing cells that do not have their activity indicator turned on.
You need to make a custom UITableViewCell by extending it. Then have a UIActivityIndicatorView as a member of that Cell. Then you can access it and control it on a cell by cell basis.
Assuming that activity view indicator tag is unique in the cell.contentView you can try something like:
UITableViewCell *cell = [tableview cellForRowAtIndexPath:indexpath];
UIActivityIndicatorView *acView = (UIActivityIndicatorView *)[cell.contentView viewWithTag:1];
//acView will be your activitivyIndicator

UITableViewCell button to expand text in a cell - how to do this?

I've got a UITableView with a list of 10 sections, each containing a textLabel.
I've limited the amount of text in each cell to 10 lines and put a button to allow to user to expand the text in the cell.
The problem is, I want to change the text on the button when it's clicked between "show more" (if cell is not expanded) and "show less" (if cell is expanded). How can I do that?
Right now I have this code in cellForRowAtIndexPath. It doesnt seem to work correct, sometimes the button displays "show less" when there are only 10 lines displayed.
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"ButtonCell"];
if (!cell) {
UIButton *readmoreButton = [UIButton buttonWithType:UIButtonTypeCustom];
[readmoreButton setTitle:#"Read More" forState:UIControlStateNormal];
[cell addSubview:readmoreButton];
}
else {
NSEnumerator *e = [cell.subviews objectEnumerator];
id object;
while (object = [e nextObject]) {
if ([object class]==UIButton.class && [((UIButton*)object).titleLabel.text isEqualToString:#"Read Less"])
((UIButton*)object).titleLabel.text = #"Read More";
}
}
The following method is called when the user clicks the button. The NSMutableArray ProposalInfo keeps track of which cell is/is not expanded.
- (void)toggleFullProposal:(id) sender {
// get cell
NSIndexPath *index = [self.tableView indexPathForCell:(UITableViewCell *)[sender superview]];
ProposalInfo *proposalInfo = [self.proposalInfoArray objectAtIndex:index.section];
if ([proposalInfo expanded]==YES) {
[proposalInfo setExpanded:NO];
[(UIButton *)sender setTitle:#"Read More" forState:UIControlStateNormal];
} else {
[proposalInfo setExpanded:YES];
[(UIButton *)sender setTitle:#"Read Less" forState:UIControlStateNormal];
}
// create index paths to reload
NSMutableArray *indexPathsToReload = [[NSMutableArray alloc] init];
[indexPathsToReload addObject:[NSIndexPath indexPathForRow:0 inSection:index.section]];
// refresh
[self.tableView reloadRowsAtIndexPaths:indexPathsToReload withRowAnimation:UITableViewRowAnimationNone];
}
When the button is clicked make sure that now you set the full text to the cell. And then only reload the cell and mind it you need to fix the height of label in layoutSubviews: and then return appropriate height for heightForRowAtIndexPath: as well.
((MyCell*)[tableview cellForRowAtIndexPath:buttonClickedindexPath]).fullText = myModel.fullText;
and in the layoutSubviews: method of the custom class cell expand the frame of the the label so as to fit the text inside, then return the appropriate height in heightForRowAtIndexPath: and that should do the trick.

Table Cell SubView Iteration Not Finding TextField

I've created a table where each cell holds both a text label and a text field. I'm adding the textfields as such [cell addSubview:passwordField]; and from a visual perspective they appear and are editable, etc....
The problem arises when I attempt to retrieve the entered values from the textfields. I iterate over the cells and try to grab the subview (IE the textfield), however, my iteration only discovers the text label.
Here is the code I'm using to search:
for(NSInteger i =0; i < [tableView numberOfRowsInSection:0]; i++){
NSIndexPath *path = [NSIndexPath indexPathForRow:i inSection:0];
UITableViewCell *cell = [tableView cellForRowAtIndexPath:path];
UIView* subView = [[cell.contentView subviews]lastObject]; // I've also tried object at index here
// Anything beyond this is just matching....
Another approach I took was recursively searching the subviews, but, again that yielded no results.
You have added your textField on subView of cell.
[cell addSubview:passwordField];
While you're trying to find it on cell.contentView.
Add your textField as a subView of cell.Contentview
[cell.contentView addSubview:passwordField];
And find it in this way -
for(UIView *view in [cell.contentView subviews])
{
if([view isKindOfClass:[UITextfield class]])
{
UITextField *textField = (UITextField *)view;
NSLog(#"%#",textField.text);
}
}
Why not have a datasource mapped to the TableView and just retrieve / update the values in the datasource. You can then call reloadRowsAtIndexPaths to load just the row you just changed. Trying to iterate through the TableView rather than just updating the datasource seems very inefficient.
Instead of UIView* subView = [[cell.contentView subviews]lastObject]; you can try to find it as:
for(UIView *view in [cell subviews])
{
if([view isKindOfClass:[UITextfield class]]){
// view is the reference to your textfield
}
}
That way you can add other UIViews as subviews and still get the reference of the textfield without having to keep track of its subview index.
2 things occur to me:
In the long run it'll be easier to create a UITableViewCell which contains a UITextField which is accessible as a property. You can either use a nib to layout the cell or do it programmatically in the cells init method. This approach will make your code easier to manage.
You need to consider cell reuse. If you are reusing cells (which you should be) then you will need store the fetch the value from the textfield before it is reused.

UITableView discloure indicator going nuts

I have a UITableView (on a UIViewController) which is pushed via a navigationController. Along with pushing, I select with which array i want to populate the table. The code for pushing is like this:
if(self.newView == nil)
{
NewView *viewTwo = [[NewView alloc] initWithNibName:#"Bundle" bundle:[NSBundle mainBundle]];
self.newView = viewTwo;
[viewTwo release];
}
[self.navigationController pushViewController:self.newView animated:YES];
newView.tableArray=newView.arrayWithOptionOne;
[newView.tableView reloadData];
All works well and the table gets reloaded every time. However in the last row of section 0, there is a switch which loads section 1.
The last row of section 1 is tappable (didSelect…) and it loads a modalView. On this last rod I added a disclosure indicator and also the blue background when tapping. The table has sliders, labels, etc. So the customization is quite long:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
UITableViewCell *cell = nil;
static NSString *kDisplayCell_ID = #"DisplayCellID";
cell = [tableView dequeueReusableCellWithIdentifier:kDisplayCell_ID];
if (cell == nil)
{
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleValue2 reuseIdentifier:kDisplayCell_ID] autorelease];
cell.selectionStyle = UITableViewCellSelectionStyleNone;
}
else
{
UIView *viewToRemove = nil;
viewToRemove = [cell.contentView viewWithTag:1];
if (viewToRemove)
[viewToRemove removeFromSuperview];
UIView *viewToRemove2 = nil;
viewToRemove2 = [cell.contentView viewWithTag:2];
if (viewToRemove2)
[viewToRemove2 removeFromSuperview];
}
if (indexPath.section==0) {
UIControl *cellValueS = [[[arrayTable objectAtIndex:indexPath.section] objectForKey:kViewKey] objectAtIndex:indexPath.row];
[cell.contentView addSubview:cellValueS];
}
if (indexPath.section==0 && indexPath.row==3) {
UIControl *cellValueL = [[[arrayTable objectAtIndex:indexPath.section] objectForKey:kViewLabel] objectAtIndex:0] ;
[cell.contentView addSubview:cellValueL];
}
if (indexPath.section==1 && indexPath.row==0){
UIControl *cellValueS = [[[arrayTable objectAtIndex:indexPath.section] objectForKey:kViewKey] objectAtIndex:indexPath.row] ;
[cell.contentView addSubview:cellValueS];
}
if (indexPath.section==1 && indexPath.row==1) {
UIControl *cellValueS = [[[arrayTable objectAtIndex:indexPath.section] objectForKey:kViewKey] objectAtIndex:indexPath.row] ;
[cell.contentView addSubview:cellValueS];
UIControl *cellValueL = [[[arrayTable objectAtIndex:indexPath.section] objectForKey:kViewLabel] objectAtIndex:0] ;
[cell.contentView addSubview:cellValueL];
}
if (indexPath.section==1 && indexPath.row==2) {
cell.selectionStyle = UITableViewCellSelectionStyleBlue;
cell.accessoryType=UITableViewCellAccessoryDisclosureIndicator;
}
return cell;
}
So far also works ok.
The problem is that when I go back to the previous view and select another option to populate the table, when it's pushed again, I see the disclosure indicator and blue selection background on other rows on the same section. I've observed that it depends on where the table is scrolled.
I've tried to understand why does it happen, but i can't. I've somehow solved the problem by setting newView to nil and releasing it and then allocating it again before it gets pushed again.
Am I doing something wrong here? or why is the disclosure indicator and tapping background appearing where they are not supposed to be?
Thanks in advance!
action of the switch
-(void)extraOptionsSwitchAction:(id)sender{
switch(extraOptionsSwitch.isOn) {
case 1:
// [self.tableView beginUpdates];
[self.tableView insertSections: [NSIndexSet indexSetWithIndex:1] withRowAnimation:UITableViewRowAnimationAutomatic];
[self.tableView reloadSections:[NSIndexSet indexSetWithIndex:0] withRowAnimation:UITableViewRowAnimationAutomatic];
//[self.tableView reloadData];
// [self.tableView endUpdates];
break;
case !1:
// [self.tableView beginUpdates];
[self.tableView deleteSections: [NSIndexSet indexSetWithIndex:1] withRowAnimation:UITableViewRowAnimationAutomatic];
// [self.tableView reloadSections:[NSIndexSet indexSetWithIndex:0] withRowAnimation:UITableViewRowAnimationAutomatic];
[self.tableView reloadData];
// [self.tableView endUpdates];
break;
}
}
It has to to with reusing cells. You probably don't differentiate between the different cell types in your cell creation method - so one cell that went offscreen can easily be reused for another (different type) cell. Further, you seem to add subviews over and over again - only do that when you instantiate the cell (with alloc/int), and not when configuring.
Also, for different cell types, use different identifiers (you didn't show this code).
The programming guides have good example on table views and their cells and reuse pattern. It's worth reading a couple of times - it's easy to get wrong and is a main topic for performance tuning.
Edit
Now that you added more code, another problem seems to be here:
if (indexPath.section==1 && indexPath.row==2) {
cell.selectionStyle = UITableViewCellSelectionStyleBlue;
cell.accessoryType=UITableViewCellAccessoryDisclosureIndicator;
}
You are missing an else part. The selectionStyle and accessoryType are set to what they were set to before - you miss to configure the cells correctly for all other cells than that special one.
Each cell type should really get its own identifier though. If the adding/removing of subviews work as expected is hard to tell from that code.
One thought: As you aren't really reusing a lot of the cells here anyhow you could even disable the reuse by changing
static NSString *kDisplayCell_ID = #"DisplayCellID";
cell = [tableView dequeueReusableCellWithIdentifier:kDisplayCell_ID];
to
static NSString *kDisplayCell_ID = nil;
This would just always produce a new cell. I wouldn't recommend this in the general case, though.
This is due to cellReusability, and it has, for a long time, wasted so many developers' time and effort. If you knew the concept of cell reusability, you would know that while scrolling, the index of the row and sections remain the same (although you expect it to be different for a different position on the uiTableView).
The only way is to subClass the UITableViewCell and create your own CustomUITableViewCell, and implement that with the disclosure indicator, or resist your input to just a small TableView that fits the screen and make scrollable = NO.

Clear all UITextField changes in a UITableView with a UIButton

I have a UITableView with a dozen rows, each containing a UITextField.
By default the UITextField contains a placeholder value "Add Value" if the user hasn't previously edited the text field:
UITextField *textField = [[UITextField alloc] initWithFrame:CGRectMake(158, 6, 148, 24)];
NSString *strReplacement = [valueArray objectAtIndex:indexPath.row];
if (([strReplacement length] != 0) {
textField.text = strReplacement;
} else {
textField.placeholder = #"Add Value";
}
textField.delegate = self;
[cell addSubview:textField];
[textField release];
So far so good.
I've also added a UIButton to the footer of the UITableView.
What I want is to clear all the edited values and refresh all the UITextFields in the UITableView when the user clicks the UIButton.
I can easily enough remove all objects from the valueArray but I can't figure out how to refresh all the UITableView cells to reflect the changes.
Any help is appreciated.
lq
I believe what you're looking for is
[tableView reloadData];
Your solution feels weird. Filipe's right that the correct way to do it is with [wordsTableView reloadData], which will cause tableView:cellForRowAtIndexPath: to be called for each visible cell. That method is also called as you scroll through the table, so if reloadData isn't working, you're probably also going to end up with bugs with data not updating correctly as you change it and scroll. In your clearValues method, you're doing the same thing by calling tableView:cellForRowAtIndexPath:.
I think the real problem is in your tableView:cellForRowAtIndexPath: implementation. That method generally has 2 sections. First, you create or recycle a cell to get a reference with something like:
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier];
if (cell == nil) {
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellIdentifier] autorelease];
}
Inside that if statement is generally the only place you should be adding subviews to your cell. If dequeueReusableCellWithIdentifier: returns a cell, it should already have the subview.
Then, after that if statement, you populate or update the contents of the subviews. The problem with your original code is that it's populating the text field and adding it as a subview, assuming there isn't already a text field in the cell. So your tableView:cellForRowAtIndexPath: should look something more like this:
int textFieldTag = 100;
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier];
if (cell == nil) {
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellIdentifier] autorelease];
UITextField *textField = [[UITextField alloc] initWithFrame:CGRectMake(158, 6, 148, 24)];
[textField setTag:textFieldTag];
[textField setDelegate:self];
[cell addSubview:textField];
[textField release];
}
UITextField *textField = [cell viewWithTag:textFieldTag];
NSString *strReplacement = [valueArray objectAtIndex:indexPath.row];
if (([strReplacement length] != 0) {
textField.text = strReplacement;
} else {
textField.placeholder = #"Add Value";
}
It looks like you may be setting the textField's tag value to the row number, presumably so you can use it in the UITextFieldDelegate. That could also lead to bugs, as if the cell from row 1 is recycled by dequeueReusableCellWithIdentifier: and becomes row 12, it's going to have an unexpected tag value. Even if it doesn't happen now, it's a bug waiting to happen, and will be tricky to troubleshoot.
Filipe's solution should also work, however, calling reloadData should be avoided wherever possible as calling this method has a high performance overhead.
You need some class that has a reference to both the UIButton instance as well as all the instances of the UITextField that you have on the screen/in the table. Sounds like the perfect job for your UITableView controller subclass!
In your code above, why don't you also add each UITextField that you create to an NSArray of text fields that lives in your UITableView controller? Then when the user presses the UIButton, the action can call some method in your controller class, which loops through all the UITextField elements in the NSArray setting the text property of each instance to #"".
Warning: If you're reusing cells then you may have to ensure that the controller's NSArray of UITextFields is being updated properly.
After a few hours of trial and error, I came up with this solution. The UIButton "Clear All" invokes the following method:
- (IBAction)clearValues:(id)sender {
// count the number of values in the array (this is the same as the number of rows in the table)
int count = [valueArray count];
// remove all values from the array (deletes any user added values):
[self.valueArray removeAllObjects];
UITableViewCell *cell;
UITextField *textField;
// loop through each row in the table and put nil in each UITextField:
for (NSUInteger i = 0; i < count; ++i) {
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:i inSection:0];
cell = [self.wordsTableView cellForRowAtIndexPath:indexPath];
// NOTE: Make sure none of your tags are set to 0 since all non-tagged objects are zero.
// In table construction, your textFieldTags should be: textField.tag=indexPath.row+1;
textField = (UITextField*)[cell viewWithTag:i+1];
textField.text = nil;
}
}