Cocoa-Touch: UIPickerView viewForRow is changing row orders - iphone

I have a UIPickerView. I'm customizing it's rows via it's delegate's viewForRow as follows:
- (UIView *)pickerView:(UIPickerView *)pickerView viewForRow:(NSInteger)row forComponent:(NSInteger)component reusingView:(UIView *)view {
if (view) {
return view;
} else {
NSString *s = [datePickerValues objectAtIndex:row];
UILabel *l = [[[UILabel alloc] initWithFrame:CGRectMake(0, 0, 300, 44)] autorelease];
// l.text = s;
l.text = [NSString stringWithFormat:#"%# r:%ld", s, row];
l.font = [UIFont boldSystemFontOfSize:18];
l.textAlignment = UITextAlignmentCenter;
l.backgroundColor = [UIColor purpleColor];
return l;
}
}
When I spin it around for a bit, the rows get mixed up.
I even get the same row two or more times, and sometimes missing rows. The row count is always at 10 tho, it just seems to be calling the delegate's viewForRow method with a wrong row parameter.
I'm using the row paremeter to identify the rows. (as the documentation says). It has a single component, so the component param is always 0, I've verified this with the debugger.
Another weird thing, according to the documentation, once I create the view for a specific row, the delegate's view parameter will have that view, but using the debugger I've seen that the delegate's viewForRow is sometimes called more than once for the same row with a view = nil.
Any idea why this strange behavior? I'm new to cocoa and Obj-C, am I doing something wrong?
EDIT:
From the docs:
view - A view object that was previously used for this row, but is now hidden and cached by the picker view.
So this means that the 2nd time the delegate's viewForRow is called for a specific row, it will have the view returned on call 1. This makes sense since this way the delegate wouldn't have to re-create the view over and over as the user spins the control.
I've verified that in fact the viewForRow is called EVERY time a row is displayed, even if it was previously displayed.
What then is the use for the view parameter? The two answers so far don't seem to be valid.

You must update the data in the view each time this method is called. By not updating you are returning stale/duplicated data.

Looks like the problem is that UIPickerView tries to reuse your already created views for new rows for performance reasons. In this case ( (view != nil) ) you just return the same view you had for some previous row.
If your view is always is a UILabel you can rewrite your code like that (sorry didn't compile it):
- (UIView *)pickerView:(UIPickerView *)pickerView viewForRow:(NSInteger)row forComponent:(NSInteger)component reusingView:(UIView *)view {
NSString *s = [datePickerValues objectAtIndex:row];
UILabel *l = (view != nil)? view : [[[UILabel alloc] initWithFrame:CGRectMake(0, 0, 300, 44)] autorelease];
l.text = [NSString stringWithFormat:#"%# r:%ld", s, row];
l.font = [UIFont boldSystemFontOfSize:18];
l.textAlignment = UITextAlignmentCenter;
l.backgroundColor = [UIColor purpleColor];
return l;
}
it must work ok.

I see this is a very old question, but I was having the same issue with iOS7 and could not find a satisfactory answer. When I tried to tap into the 'row' parameter from the viewForRow delegate method, it would return incorrect and mixed up values.
I was able to solve this issue by doing the following,
- (UIView *)pickerView:(UIPickerView *)pickerView viewForRow:(NSInteger)row forComponent:(NSInteger)component reusingView:(UIView *)view {
...
//Create a label
//Set a tag for label
//Add label to view as a subview
//return view
if (component == 1) {
view = [[UIView alloc] init];
view.backgroundColor = [UIColor clearColor];
correctRowLabel = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, 30, 20)];
[correctRowLabel setText:[NSString stringWithFormat:#"%li", row]];
[correctRowLabel setTag:100];
[view addSubview: correctRowLabel];
return view;
}
After the component is setup as desired, at the beginning of the viewForRow delegate method, implement the following to view the correct row values,
- (UIView *)pickerView:(UIPickerView *)pickerView viewForRow:(NSInteger)row forComponent:(NSInteger)component reusingView:(UIView *)view {
UILabel* correctRow = (UILabel*)[pickerView viewWithTag:100];
NSInteger correctRowValue = [correctRow.text integerValue];
NSLog(#"Correct row is: %li",correctRowValue);
UILabel* correctRowLabel = nil;
...
EDIT:
I wanted to add that the rowHeightForComponent must be set to default and the label must be reset at the top of viewForRow method. It isn't working perfectly but it is significantly better than before. If i learn of anything else I'll update this post.

Related

Custom Cell, only last cell gets [layoutSubviews]?

I'm creating a Settings View for my app, and in that view is a UITableView. I'm creating custom cells to meet my needs, but I'm having issues - only the last cell is getting [layoutSubviews]. Am I doing something wrong?
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
//int type = (indexPath.row == 0?1:0);
//if(indexPath.row == 6) type = 2;
NSLog(#"row %i created", indexPath.row);
TableCell *cell = [[TableCell alloc] initWithType:indexPath.row];
cell.textLabel.text = #"Test cell";
return cell;
}
And in my custom cell:
#implementation TableCell
UIImageView *shadowView;
int row;
- (id) initWithType:(int)type {
row = type;
self = [super initWithStyle:UITableViewCellStyleDefault reuseIdentifier:nil];
self.backgroundColor = [UIColor clearColor];
self.backgroundView = [[UIView alloc] init];
UIImage *shadowImage = [UIImage imageNamed:#"CellShadow"];
shadowImage = [shadowImage resizableImageWithCapInsets:UIEdgeInsetsMake(14, 14, 14, 14)];
shadowView = [[UIImageView alloc] initWithImage:shadowImage];
[self.contentView addSubview:shadowView];
//[self.contentView sendSubviewToBack:shadowView];
NSLog(#"agreed, row %i created", row);
[self layoutSubviews];
return self;
}
- (void) layoutSubviews {
NSLog(#"row: %i", row);
[super layoutSubviews];
shadowView.frame = CGRectMake(
0, 0,
self.contentView.frame.size.width,
self.contentView.frame.size.height
);
}
#end
Continuously, only the last cell #6, is reported when I rotate, or when layoutSubviews should be called. Any suggestions?
Do not call layoutSubviews directly. Use [self setNeedsLayout] or [self layoutIfNeeded]. But do not call these at all in the cell's init method.
Also, do not call [[TableCell alloc] initWithType:indexPath.row]; directly, either. Instead, use...
- (id)dequeueReusableCellWithIdentifier:(NSString *)identifier forIndexPath:(NSIndexPath *)indexPath
Once you've built that cell, you can tell it it's row, but be aware that the cells get recycled as the table scrolls, so you must update that value on every call to cellForRowAtIndexPath.
The cells ought to get layout again (without you making any calls direct or indirect) when the table view is resized.
See the tableview doc here.
You should never call layoutSubviews directly, it will be called automatically by iOS once the cell is ready to display. You should also deque the cell as #danh is recommending. If you're not very comfortable with all this, then I'd really recommend you have a look at the free Sensible TableView framework, which automates creating these kind of settings views (I create mine in a couple of lines, really).
The issue was of my own poor code. Using cell.backgroundView helped a lot here.
Never Call layoutSubviews by yourself. It will be called when ever frames of subview in cell are changed. Even if just change the text of labels in your custom cell wont call layoutSubviews. Ue the deque of cells for reusing for better performance. As it wont allocate cell every time. And in you code looks like has lot of memory issues since cell allocated wont be released and new cell is created.

Add UITableViewCell with UITextField Dynamically

I'm attempting to have a UITableView that I can dynamically add and remove rows from, and the rows have a UITextField in them. For adding the rows, I'm using the code:
- (void) addRow
{
[nameArray addObject:[NSString stringWithFormat:#"%i", x]];
[self.tableView reloadData];
x++;
}
And I'm just doing a count of nameArray to get how many rows I have in my tableView. Then, inside of cellForRowAtIndexPath, I've got the following code
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
...
/*Default Apple-y stuff*/
...
if ([indexPath row] == 0) {
playerTextFieldZero = [[UITextField alloc] initWithFrame:CGRectMake(20, 10, 185, 30)];
playerTextFieldZero.adjustsFontSizeToFitWidth = YES;
playerTextFieldZero.placeholder = #"Name";
playerTextFieldZero.textColor = [UIColor blackColor];
playerTextFieldZero.returnKeyType = UIReturnKeyDone;
playerTextFieldZero.backgroundColor = [UIColor whiteColor];
playerTextFieldZero.autocorrectionType = UITextAutocorrectionTypeNo; // no auto correction support
playerTextFieldZero.textAlignment = UITextAlignmentLeft;
playerTextFieldZero.tag = 0;
playerTextFieldZero.delegate = self;
playerTextFieldZero.clearButtonMode = UITextFieldViewModeNever; // no clear 'x' button to the right
[playerTextFieldZero setEnabled: YES];
[cell addSubview:playerTextFieldZero];
[playerTextFieldZero becomeFirstResponder];
[playerTextFieldZero release];
}
...
/*More of those in here*/
...
return cell;
}
I've got multiple issues with this code. The first issue is I'm doing a preset number of UITextFields, so that I can call them all in textFieldShouldReturn. Is there a good way for me to generate UITextFields that will return when I press the done key?
The second biggest issue with the way I'm doing this right now is my UITextFields get cleared every time I add a new one. Any idea why?
To solve your first issue I would begin by pulling the UITextField creation code into a method..
- (UITextField*)textFieldForCell:(UITableViewCell*)cell withDelegate:(id<UITextFieldDelegate>*)delegate {
UITextField textField = [[UITextField alloc] initWithFrame:CGRectMake(20, 10, 185, 30)];
textField.delegate = self;
....
[cell addSubview:playerTextFieldZero];
[textField release];
}
Then invoke the new method in your tableView:cellForRowAtIndexPath: method...
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
...
// Custom initialization code
[self textFieldForCell:cell withDelegate:self];
}
Now to make sure that your UITextField's respond to the return key implement the textFieldShouldReturn: method of your UITextFieldDelegate (probably your UITableViewController) to always return true...
-(bool)textFieldShouldReturn:(UITextField*)textField {
return YES;
}
As for your second issue, I believe this is a result of directly invoking reloadData. This will force your UITableView to recreate its cells. This in turn recreates your UITextFields and you subsequently lose their state/text. I think your next logical step will be to introduce a model (NSMutableArray) that stores the state of each UITextField. You could begin by saving the text of the field into the array upon the UITextFieldDelegate receiving the textFieldShouldReturn message.

pickerview for iphone that looks like country select feature

I want to create a picker that looks and works like the country select field of itune store account.
Here is what I am talking about.
http://i38.tinypic.com/5p0w91.jpg
This picker doesn't have a row highlighted. It has "correct" sign marked in front of selected row. user can scroll this picker and then select the row so "correct" sign will appear in front of newly selected row.
Can someone please help?
This can be accomplished relatively easily using a picker with custom component views. Use an instance variable to keep track of your selected row, and change the color of your label accordingly. If you wanted to include the check mark, you'd need to go a step further and use a custom subclass of UIView rather than a simple UILabel.
#interface ViewContainingPicker
{
NSUInteger mySelectedRow;
}
#end
#implementation ViewContainingPicker
// Init, Picker setup, etc
- (UIPickerView *)myPickerView
{
// Create picker, set mySelectedRow to NSNotFound
mySelectedRow = NSNotFound;
return myPickerView;
}
- (UIView *)pickerView:(UIPickerView *)pickerView viewForRow:(NSInteger)row forComponent:(NSInteger)component reusingView:(UIView *)view
{
UILabel *label = (UILabel *)view;
if (nil == label) {
UILabel *label = [[[UILabel alloc] initWithFrame:CGRectMake(0, 0, PICKER_WIDTH, PICKER_ROW_HEIGHT)] autorelease];
}
label.text = #"Label for this row";
// Selected Row will be blue
if (row == mySelectedRow) {
label.textColor = [UIColor blueColor];
} else {
label.textColor = [UIColor blackColor];
}
return label;
}
- (void)pickerView:(UIPickerView *)pickerView didSelectRow:(NSInteger)row inComponent:(NSInteger)component
{
// Just set selected component and reload, color will change in dataSource pickerView:viewForRow:forComponent:reusingView:
mySelectedRow = row;
[pickerView reloadComponent:component];
}
I'm not familiar with coding apps, but for a browser-based UI (Safari), you would just use a simple drop-down menu:
<select>
<option>Country 1</option>
<option>Country 2</option>
...
<option>Country N</option>
</select>
Guessing whatever is the equivalent in the iPhone SDK should work the same.

UITableView section header and section footer not updating (redraw problem)

UPDATE 4.0
Seems like iOS 4.0 changed something here. Same code producing incorrect backgrounds for section header in the described scenario is working with 4.0 according to my first quick check!
Original
I have a UITableView grouped style with custom header and footer view. Inside the footer I put a UILabel and a UIButton.
Clicking on the button hides or show some rows, updates the UILabel in the footer view and finally resizes footer view.
Basically everything is working fine. BUT the text ion the label is not updated on the screen. It is updated in the UILabel text property, but only if I scroll the section footer out of the visible area and scroll it back, it is updated. So it's a typical redraw problem here of the UITableView.
I tried every method to force update like needsLayout etc. Nothing helped.
I have seen some related questions but with some different behaviour and no solution. Any help/ideas?
Thanks, Gerd
UPDATE:
My problems occurs with section footer, so here is my viewForFooterInSection.
Basically I want to collapse/expand a section, but not completely (that was an easy thing) instead only the empty cell (ItemSize empty). The footerView is large if it is collapsed and will shrink if it is expanded. Furthermore the label text will change.
- (UIView *)tableView: (UITableView *)tableView viewForFooterInSection: (NSInteger)section{
NSLog(#"viewForFooterInSection section:%i", section);
UIButton *myView;
UILabel *label;
if ([[[self.sectionStatus objectAtIndex:section] valueForKey:#"collapseStatus"] isEqual:#"collapse"]){
myView = [[UIButton alloc] initWithFrame:CGRectMake(0, 0, 320, 52)];
[myView setBackgroundImage:[UIImage imageNamed:#"ItemViewFooter.png"] forState:UIControlStateNormal];
label = [[UILabel alloc] initWithFrame:CGRectMake(20, 32, 300, 20)];
label.text = NSLocalizedString(#"list_expand",#"");
} else { //is expanded
myView = [[UIButton alloc] initWithFrame:CGRectMake(0, 0, 320, 21)];
[myView setBackgroundImage:[UIImage imageNamed:#"ListCollapseExpand.png"] forState:UIControlStateNormal];
label = [[UILabel alloc] initWithFrame:CGRectMake(20, 1, 300, 20)];
label.text = NSLocalizedString(#"list_collapse",#"");
}
myView.tag=section;
[myView addTarget:self action:#selector(collapseExpandAction:) forControlEvents:UIControlEventTouchUpInside];
myView.backgroundColor = [UIColor clearColor];
myView.adjustsImageWhenHighlighted = NO;
myView.showsTouchWhenHighlighted = YES;
label.textColor = FONTCOLOR;
label.font = [UIFont systemFontOfSize:14];
label.numberOfLines = 1;
label.textAlignment = UITextAlignmentCenter;
label.backgroundColor = [UIColor clearColor];
[myView addSubview:label];
return myView;
};
In the button action method I store status of section collapse/expand and the number of displayed rows. Than I delete or insert rows. (It has to be with insert/delete because I need the animation).
- (void) collapseExpandSection: (NSInteger) section{
NSMutableArray *paths = [NSMutableArray arrayWithCapacity:10];
NSInteger row;
NSInteger numberOfDisplayedItems=[[[self.sectionStatus objectAtIndex:section] valueForKey:#"numberOfDisplayedRows"] intValue];
id <NSFetchedResultsSectionInfo> sectionInfo = [[fetchedResultsController sections] objectAtIndex:section];
NSInteger numberOfAllItems=[sectionInfo numberOfObjects];
Item *tmpItem=nil;
NSSet *itemsWithSizes=nil;
//filter not used cells
for ( row = 0; row < numberOfAllItems; row++ ) {
tmpItem=[fetchedResultsController objectAtIndexPath:[NSIndexPath indexPathForRow:row inSection:section]];
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"itemSize != nil"];
NSSet *itemsWithSizes = [tmpItem.itemSizes filteredSetUsingPredicate:predicate];
if ([itemsWithSizes count]==0){
[paths addObject:[NSIndexPath indexPathForRow:row inSection:section]]; //all unused cells
};
}
if (numberOfDisplayedItems == numberOfAllItems){ //currently all shown => Collapse
[self.tableView beginUpdates];
[[self.sectionStatus objectAtIndex:section] setValue:[NSNumber numberWithInt:(numberOfDisplayedItems-[paths count])] forKey:#"numberOfDisplayedRows"];
[[self.sectionStatus objectAtIndex:section] setValue:#"collapse" forKey:#"collapseStatus"];
[self.tableView deleteRowsAtIndexPaths:paths withRowAnimation:UITableViewRowAnimationFade];
[self.tableView endUpdates];
} else { //Not all shown so expand with the unused cells
[[self.sectionStatus objectAtIndex:section] setValue:[NSNumber numberWithInt:(numberOfDisplayedItems+[paths count])] forKey:#"numberOfDisplayedRows"];
[[self.sectionStatus objectAtIndex:section] setValue:#"expand" forKey:#"collapseStatus"];
[self.tableView beginUpdates];
[self.tableView insertRowsAtIndexPaths:paths withRowAnimation:UITableViewRowAnimationFade];
[self.tableView endUpdates];
}
return;
};
Doing all this works fine in general. After the blocks begin/endupdate the viewForFooter is called for every section and the label text is set correct in the property. However the display doesn't update correctly. As soon as a redisplay is forced (srolling out- scrolling in) the display is OK.
There 2 problems.
First problem is that section footer not updated.
Try call [tableView reloadData] or [tableView reloadSections:sections withRowAnimation:UITableViewRowAnimationFade] after your update (may be with dalay).
Second problem is memory leaks in myView and label.
Also why do you use label when you can use button's internal label?
P.S. Don't allocate UIButton object directly because it is factory. Call [UIButton buttonWithType:UIButtonTypeCustom] instead.
Upd: Another way to update is to update footer directly by accessing footer views.
- (void) collapseExpandSection: (NSInteger) section{
Check that section is actualy your button
- (void) collapseExpandSection: (UIButton*) sender{
// Do update of sender here
// Do other stuff
}
Also you can try next trick: create UIView object in delegate, add your button and label on it and return instaed of buttom view itself.
My problem was that I was expanding the Section header to show a search bar, but it wouldn't redraw the view until I scrolled the UITableView.
I had my own SectionHeader class that subclassed UIView and controlled the searching stuff.
After my animation, I just used this to force an update. It's not pretty but it works.
CGPoint point = CGPointMake(((UIScrollView *)self.superview).contentOffset.x,
((UIScrollView *)self.superview).contentOffset.y+1);
[((UIScrollView *)self.superview) setContentOffset:point animated:NO];
point = CGPointMake(((UIScrollView *)self.superview).contentOffset.x,
((UIScrollView *)self.superview).contentOffset.y-1);
[((UIScrollView *)self.superview) setContentOffset:point animated:NO];
Basically force the UITableView to scroll down 1 pixel and up 1 pixel.
I had an issue just like this where I wanted to update the section header after inserting a new row. I found that calling tableView reloadSections() method with an animation setting of .None after I call the insertRows method worked for me (both calls in the same tableView update block). I got the insert animation I wanted and also the section header was updated.

UIPickerView added to uitableviewcontroller.view won't rotate

I have a UIPickerView as subview in a UITableViewController, which I want to slide up in place when a certain row is clicked. I have implemented the necessary dataSource and delegate methods, but the pickerview acts weirdly. It acts as if it were mechanically jammed, it cannot rotate properly, it just moves a little. If I try to spin it a few times I get this message in the debugger console:
SendDelegateMessage: delegate failed
to return after waiting 10 seconds.
main run loop mode:
UITrackingRunLoopMode If you were not
using the touch screen for this entire
interval (which can prolong this
wait), please file a bug.
I googled for this, to no avail.
Anyway, here is the relevant code. As you may guess, this is a number picker (to choose a percentage value).
- (void)viewDidLoad {
[super viewDidLoad];
percentPicker = [[UIPickerViewalloc] initWithFrame:CGRectMake(0,420, 320, 200)];
percentPicker.delegate = self;
percentPicker.dataSource = self;
percentPicker.showsSelectionIndicator = YES;
[self.view addSubview:percentPicker];
}
- (void)viewWillAppear:(BOOL)animated {
// sets the rows to the appropriate value
// etc
}
- (void)startEditingPercentage {
[UIView beginAnimations :nilcontext:NULL];
[UIView setAnimationBeginsFromCurrentState:YES];
[UIViewsetAnimationDuration:kPickerAnimationDuration ];
percentPicker.transform = CGAffineTransformTranslate(CGAffineTransformIdentity, 0, -220.0);
[UIViewcommitAnimations];
}
- (void)stopEditingPercentage {
NSLog(#"stopEditingPercentage");
[UIView beginAnimations :nilcontext:NULL];
[UIViewsetAnimationBeginsFromCurrentState:YES];
[UIViewsetAnimationDuration:kPickerAnimationDuration];
percentPicker.transform = CGAffineTransformIdentity;
[UIView commitAnimations];
}
#pragma mark UIPickerView delegate and dataSource methods
- (NSInteger)numberOfComponentsInPickerView:(UIPickerView *)pickerView {
return4;
}
- (NSInteger)pickerView:(UIPickerView *)pickerView numberOfRowsInComponent:(NSInteger)component {
return10;
}
- (CGFloat)pickerView:(UIPickerView *)pickerView widthForComponent:(NSInteger)component {
return44;
}
- (UIView *)pickerView:(UIPickerView *)pickerView viewForRow:(NSInteger)row forComponent:(NSInteger)component reusingView:(UIView *)view {
UILabel *retval = (UILabel *)view;
if (!retval) {
retval= [[UILabel newLabelWithPrimaryColor :[UIColorblackColor ] selectedColor:[ UIColor blackColor] fontSize:22bold:YEStransparent:YES] autorelease];
}
retval.frame = CGRectMake(0, 0, 44, 44);
retval.textAlignment = UITextAlignmentCenter;
retval.backgroundColor = [UIColor clearColor];
retval.text = [NSStringstringWithFormat:#"%i", row];
if (component > 1 ) {
// rows 2 and 3 are decimal, white on black
retval.backgroundColor = [UIColor blackColor];
retval.textColor = [UIColor whiteColor];
}
return retval;
}
- (void)pickerView:(UIPickerView *)pickerView didSelectRow:(NSInteger)row inComponent:(NSInteger)component {
NSString *selectedValue;
selectedValue= [NSString stringWithFormat:#"%i%i.%i%i" ,[percentPickerselectedRowInComponent :0], [ percentPickerselectedRowInComponent: 1], [percentPickerselectedRowInComponent:2], [percentPickerselectedRowInComponent:3]];
NSNumberFormatter *formatter = [[NSNumberFormatter alloc] init];
NSString *trimmedValue = [[formatter numberFromString:selectedValue] stringValue];
percentValue.text = trimmedValue;
[formatter release];
}
As you see, I use the transform property to move the picker in and out (down) my main view; the startEditing and stopEditing methods are triggered by selection in the teble view. Yet, for the debugging purposes, I eliminated these transitions , and left the picker on top of the table, but nothing changed. I also commented the didSelect method, but this also didn't change anything.
By the way, the same picker-view construction code vorkw allright in another view of the same app.
Any suggestion?
Cheers,
You aren't really adding the picker to UITableView using the code in viewDidLoad. UITableView can only have controls/rows in UITableViewCells and these are specified in the cellForRowAtIndex method. We cannot add controls to UITableView like we do to UIView.
Try using a subclass of UIView instead and add the UITableView and the picker to this UIView subclass.
In case you want to use a UITableViewController subclass only, create a custom cell and add the picker to this custom cell. But then you won't be able to hide/unhide this cell if you add it statically. Then you'll have to use reloadData. I would suggest using the UIView subclass.