I'm trying to properly set the target of a UIButton touched event in a subclass of UITableCell (designed in IB) that will delete that cell. However, when ran in the simulator, I get the following error:
* Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '* - [__NSPlaceholderArray
initWithObjects:count:]: attempt to insert nil object from objects[0]'
It gets to the method fine, but it always crashes right after the NSArray arrayWithObject line. It seems that the cause of this is because the button passed in to the target method is always nil.
I'm assuming this is a memory issue, but I'm quite baffled as to how to fix it. Do I just need to create the cell entirely programmatically to get this to work, or is there an easy way to somehow specify the target of the buttons action as the main ViewController from Interface Builder?
Here's where the cells are created in the view controller:
-(UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
if(tableView==songList){
static NSString *SimpleTableIdentifier = #"SimpleTableIdentifier";
SongCell *cell = (SongCell *)[tableView dequeueReusableCellWithIdentifier:SimpleTableIdentifier];
if(cell == nil) {
NSArray *nib = [[NSBundle mainBundle] loadNibNamed:#"SongCell" owner:self options:nil];
cell = [nib objectAtIndex:0];
[cell.addButton addTarget:self action:#selector(addToPlaylist:) forControlEvents:UIControlEventTouchUpInside];
}
NSUInteger row = [indexPath row];
cell.songname.text = #"test";//[songListData objectAtIndex:row];
return cell;
}
return nil;
}
And here's the target method:
-(void)addToPlaylist:(id)button{
SongCell *song = (SongCell *)[(UIButton *)button superview];
NSIndexPath *indexPath = [self.songList indexPathForCell:song];
NSArray *rowToMove = [NSArray arrayWithObject:indexPath];
[self.songList beginUpdates];
[self.songList deleteRowsAtIndexPaths:rowToMove withRowAnimation:UITableViewRowAnimationFade];
[self.songList endUpdates];
}
Any help you can offer would be greatly appreciated.
The error is b/c indexPath must be nil. There are a number of suspect things in your code sample. What is your implementation of SongCell? When you add subviews to a cell you should add them to the cell's contentView vs directly to the cell. It is likley that [button superview] is not returning the cell and therefore the next line is returning nil for the indexPath.
A few other thoughts:
It looks like you only need the row number when the button is pressed. You could store the rowIndex + 1 in the button tag property and then retrieve it - 1 in addToPlaylist:. You need to +/- b/c the tag property should be > 0. Alternatively use a UIButton subclass that has a property to store your indexPath.
If you're going to delete a row via deleteRowsAtIndexPaths:withRowAnimation: you need to first delete the same row from the data source to maintain the integrity of the table.
If you are only doing a single deletion, you don't need beginUpdates/endUpdates.
Related
I have a prototype table in my app witch I populate with a customTableViewCell class with a UITextField inside.
In my navigation bar I got a save button.
The question is, how to access this dynamic created cell's to get the UITextField content?
This is my code, you can see that I tried to use NSMutableArray
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"customTableCell";
customTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
[self.pfCells addObject:cell];
if(cell == nil)
{
cell = [[customTableViewCell alloc]
initWithStyle:UITableViewCellStyleDefault
reuseIdentifier:CellIdentifier];
}
// Configuration
cell.lblName.text = [self.pfFields objectAtIndex: [indexPath row]];
cell.txtType = [self.pfTypes objectAtIndex: [indexPath row]];
if ([[self.pfTypes objectAtIndex:[indexPath row]] isEqualToString: #"n"]) {
[cell.txtField setKeyboardType:UIKeyboardTypeNumberPad];
} else if ([[self.pfTypes objectAtIndex:[indexPath row]] isEqualToString: #"m"]) {
[cell.txtField setKeyboardType:UIKeyboardTypeEmailAddress];
}
return cell;
}
Here's another way to save content from a UITextField contained in a UITableViewCell:
Inside tableView:cellForRowAtIndexPath: set the delegate and a tag for txtField
Implement textFieldDidEndEditing: check for a UITextField tag value an save data in a private variable
Reload UITableView
The biggest advantage of this implementation if the fact that you doesn't need to iterate over whole tableview everytime you change a textfield value.
Quick answer:
#pragma mark - UITextFieldDelegate
- (void)textFieldDidEndEditing:(UITextField *)textField
{
// grab the row we are working on
NSIndexPath *indexPath = [self.tableView indexPathForSelectedRow];
// remove the old key/value pair if it exists and add the new one
[self.modelDictionary removeObjectForKey:indexPath];
[self.modelDictionary setObject:textField.text forKey:indexPath];
}
Be sure to add cell.txtField.delegate = self when configuring your cell. Then in your save button, you'd iterate through the dictionary and save the values -- or just save the dictionary itself.
Also, if you are targeting iOS6 or later, use dequeueReusableCellWithIdentifier:forIndexPath: as this method guarantees a cell is returned and resized properly, so you don't have to check for nil and manually init your cell.
Longer answer:
You generally never want to store your model in your view as you are doing. Aside from it breaking the MVC design patterns, it also causes issues with UITableViews. Specifically, a UITableViewCell will be recycled when it scrolls off the screen. So any values you have in those fields are lost. While you can get away with doing this if you only have visible rows that never scroll off the screen, I would encourage you to avoid this approach altogether.
Instead, you should store the values entered into the textboxes in your model object. The easiest way to do this is to use UITextFieldDelegate's textFieldDidEndEditing: to grab the values after the user enters them, then add these values to your model. You model could be something as simple as an NSDictionary using the indexPath as the key.
So I have a subclass UITableViewCell named MCProductCell, which is loaded from a NIB. The problem is that when the table is released, the dealloc method of my custom cell is not called even once.
Here is some sample code:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *cellIdentifier = #"MCProductCellIdentifier";
MCProductCell *cell = (MCProductCell *)[tableView dequeueReusableCellWithIdentifier:cellIdentifier];
// Boolean value needed to determine if it is a reused cell or not. If it's not reused we have
// to start the thread that loads the image. For reused cells, that thread is started at the
// end of the scrolling
BOOL recycled = YES;
if (cell == nil) {
NSLog(#"cell alloc");
recycled = NO;
NSArray *nib = [[NSBundle mainBundle] loadNibNamed:#"MCProductCell" owner:self options:nil];
cell = [nib objectAtIndex:0];
}
MCProduct *product = [products objectAtIndex:indexPath.row];
cell.product = product;
cell.cartViewController = self;
cell.productImage = product.cachedThumbnailImage;
if (product.cachedThumbnailImage == nil) {
cell.productImage = [ViewControllerUtils getDefaultImage];
if (!recycled)
[NSThread detachNewThreadSelector:#selector(loadImage:) toTarget:cell withObject:cell.product.imageThumbnailUrl];
}
return cell;
}
And for some reason, when I first present my UIViewController, that contains the table, the dealloc method of my custom cell is called ONCE.
The problem is that in the dealloc method I want to remove the cell as an observer, and if it isn't called, then the cell isn't removed as an observer.
Also the tableview is an outlet.
I figured out, it must be because the retain count of the cell is not going down to 0.
Which means you have another retain.
My more experienced colleague thinks its because you are using the detachNewThreadSelector, which probably retains the cell.
He suggested you would load the image by using some type of asynchrony image such as
https://github.com/nicklockwood/AsyncImageView/
Good luck.
How is the 'cell.cartViewController' property defined? If it's retaining your controller object (self), then you probably have a retain cycle in there!
I have a UITableView, cells of which are customised in a NIB file so I can have a UILabel and UITextField.
Therefore my cellForRowAtIndexPath looks like this:
- (UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
SectionCustomCell *cell = (SectionCustomCell *)[tableView
dequeueReusableCellWithIdentifier: #"Section"];
NSArray *nib = [[NSBundle mainBundle] loadNibNamed:#"SectionCustomCell"
owner:self
options:nil];
cell = [nib objectAtIndex:0];
// Set the label value
[[cell inputLabel] setText:#"something"];
// Set the textfield tag property
}
Therefore for a given section/row I am going to assign some text to the UILabel, defined as inputLabel, and have a UITextField, called inputTextField get some text from the user.
My plan is to set the tag property of the UITextField so I can determine which field I am getting in the delegate textFieldDidEndEditing.
Now my problem, if I put this code:
UITextField *textField = nil;
for (UIView *oneView in cell.contentView.subviews)
{
if ([oneView isMemberOfClass:[UITextField class]])
textField = (UITextField *)oneView;
}
textField.tag = [indexPath row];
the tag property is set correctly. (I know this from a NSLog statement). However if I do the following it is not set correctly. It is always 1 as defined in IB.
cell.inputTextField.tag = [indexPath row];
but to me this should work. I am doing the same principle with the setting the labels text. Can someone help me to understand why it doesn't work?
I'm new to iOS so go gentle :-)
Thanks
Mike
Make sure you have connected the textfield and the IBOutlet in IB.
If that doesnt work try putting this in the code:
NSLog(#"%i", cell.inputTextField == nil);
If it prints 1 to the console then it means the inputTextField is nil so somewhere between the nib file, your custom class and the tableview datasource the connection is getting lost. But as I first said this is most likely the textfield is not connected properly in IB.
i have set up a tableview with custom cells. customCell is a class.
heres the code for a more accurate view:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
NSManagedObject *object = (NSManagedObject *)[entityArray objectAtIndex:indexPath.row];
NSString *cellIdentifier = [NSString stringWithFormat:#"asd%d", indexPath.row];
customCell *cell = [[tableView dequeueReusableCellWithIdentifier:cellIdentifier] autorelease];
//i tried setting a tag but dunno how to call it afterwards
[cell setTag:indexPath.row];
if (cell == nil) {
NSArray *topLevelObjects = [[NSBundle mainBundle] loadNibNamed:#"customCell" owner:self options:nil];
cell = [topLevelObjects objectAtIndex:0];
}
if (cell.imagen != nil) {
[[cell imageView] setImage:[cell imagen]];
} else { /* setup image... */ }
}
-(void) webImageReady:(WebImage *)downloadedImage imageView:(UIImageView *)imageView cellTag:(NSInteger *)cTag
{
// This is the part where i want to access cell.imagen, this is actually wrong...
[[[imageView.superview viewWithTag:cTag] imagen] setImagen:downloadedImage.Image];
[imageView setImage:downloadedImage.Image];
}
Ok. now i want to access (reference) the cell.imagen property from a method outside cellForRowAtIndexPath, more precisely at a selector for a download finished (delegated)
Thanks in advance!
Do it inside cellForRowAtIndexPath if the image is downloaded, and on successful download of the image do [tableview setNeedsDisplay]
You shouldn't refer to the cell outside the cell creation method, you should consider the case the cell was rendered but while getting the image was scrolled out the dealloced or even reused for another cell, one way to solve it is to have image view array or something similar.
I think you should try using a third party lib that already doing it(among other things) called Three20. It have an object call TTImageView that gets a URL and loads it in the background, it solves all of the cases along with optimized caching
I'm using indexPath.row do determine in which row of my tableview I do something. The title of my cells is containing a number which should be 1 in the first row and 18 in the last row, so I have 18 rows. This works for the first 11 rows, but after that, I have numbers in the title which seem to be generated randomly! Sometimes 16, then 5, then 18, then 12... and so on.
What's the problem with it/why does the indexPath.row variable behave like that?
My cellForRowAtIndexPath method:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *MyIdentifier = #"MyIdentifier";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:MyIdentifier];
if (cell == nil) {
[[NSBundle mainBundle] loadNibNamed:#"myCell" owner:self options:nil];
cell = cell0;
self.cell0 = nil;
}
UILabel *label;
label = (UILabel *)[cell viewWithTag:1];
label.text = [NSString stringWithFormat:#"Cell %d", indexPath.row];
return cell;
}
Any more suggestions on how to solve the problem? I didn't get it working until now...
// Update with more code:
Here is how I declare the cell. It is in an XIB file (template "empty XIB") in which I just put the cell from the library in IB.
#interface myViewController : UITableViewController {
UITableViewCell *cell0;
}
#property (nonatomic, retain) IBOutlet UITableViewCell *cell0;
Then, at the top of the myViewController.m file:
#synthesize cell0;
My cellForRowAtIndexPath method is already posted above. It is equal to the cellForRowAtIndexPath method in the SDK documentation, and in Apple's example, it seems to work.
What are you trying to accomplish with cell0?
cell = cell0;
self.cell0 = nil;
It looks like you're creating a new cell, but somehow deciding to use an old one. The real culprit looks like the code that is loading the cell actually getting assigned anywhere.
Try just this instead:
if (cell == nil) {
cell = [[NSBundle mainBundle] loadNibNamed:#"myCell" owner:self options:nil];
}
Or perhaps:
if (cell == nil)
{
// TODO: try to avoid view controller
UIViewController *vc = [[UIViewController alloc] initWithNibName:#"IndividualContractWithResult" bundle:nil];
cell = (IndividualContractWithResult_Cell *) vc.view;
[vc release];
}
To would be easier to answer if you give the code where you create cells for your table view. It looks that there's a problem with reusing cells - you reuse previously created cells without setting a new value to it.
It sounds like you are not re-using cells but creating new ones when there are cells available. Look at the sample code for dequeueReusableCellWithIdentifier.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"MyCell"];
if (cell == nil) {
cell = [[[UITableViewCell alloc] initWithFrame:CGRectZero reuseIdentifier:#"MyCell"] autorelease];
}
cell.text = <your code here>;
return cell;
}
It would seem that you're incorrectly accessing a property here:
cell = cell0;
self.cell0 = nil;
Assuming that you have an instance variable named cell0, by setting it to nil, you may be releasing it before you're ready to use it.
The proper way to do this is:
cell = self.cell0;
self.cell0 = nil;
This way, if cell0 is declared as retain, you'll automatically get an autoreleased cell0 back, whereas if you reference cell0 directly (no self.), you'll get an unretained reference, which will disappear when self.cell0 = nil is called.
The advantage of using a nib-based cell here is that you can use outlets, rather than tags, to identify subviews. You've done the heavy lifting already, you might want to just add an outlet and subclass UITableViewCell to get access to the label.
You will need to retain and autorelease cell0, otherwise when you set self.cell0 = nil, then cell0 has no known references.
cell = [[cell0 retain] autorelease];
self.cell0 = nil;
You can also do this:
cell = self.cell0;
self.cell0 = nil;
.. Since any retain properties should implement their getters with the retain/autorelease pattern.