13

I have a UITableView with a UITextField in each of the UITableViewCells. I have a method in my ViewController which handles the "Did End On Exit" event for the text field of each cell and what I want to be able to do is update my model data with the new text.

What I currently have is:

- (IBAction)itemFinishedEditing:(id)sender {
    [sender resignFirstResponder];

    UITextField *field = sender;
    UITableViewCell *cell = (UITableViewCell *) field.superview.superview.superview;

    NSIndexPath *indexPath = [_tableView indexPathForCell:cell];

    _list.items[indexPath.row] = field.text;
}

Of course doing field.superview.superview.superview works but it just seems so hacky. Is there a more elegant way? If I set the tag of the UITextField to the indexPath.row of the cell its in in cellForRowAtIndexPath will that tag always be correct even after inserting and deleting rows?

For those paying close attention you might think that I have one .superview too many in there, and for iOS6, you'd be right. However, in iOS7 there's an extra view (NDA prevents me form elaborating) in the hierarchy between the cell's content view and the cell itself. This precisely illustrates why doing the superview thing is a bit hacky, as it depends on knowing how UITableViewCell is implemented, and can break with updates to the OS.

mluisbrown
  • 14,448
  • 7
  • 58
  • 86

4 Answers4

31

Since your goal is really to get the index path for the text field, you could do this:

- (IBAction)itemFinishedEditing:(UITextField *)field {
    [field resignFirstResponder];

    CGPoint pointInTable = [field convertPoint:field.bounds.origin toView:_tableView];    

    NSIndexPath *indexPath = [_tableView indexPathForRowAtPoint:pointInTable];

    _list.items[indexPath.row] = field.text;
}
rmaddy
  • 314,917
  • 42
  • 532
  • 579
  • Nice! I like this one a lot. – Jacob Relkin Jul 20 '13 at 02:58
  • I'm marking this as the correct answer because, as you say, it neatly solves what my real goal is (which doesn't require getting the table view cell). However, I've played with setting the tag of the text field to the indexPath.row and that also works pretty well and is what I will end up using. – mluisbrown Jul 22 '13 at 13:58
  • 1
    Keep in mind that using tags for the index path only works if your table rows can't be added, removed, or reordered. – rmaddy Jul 22 '13 at 15:26
  • @maddy are you sure? I did some quick tests in a table view where the cells can be reordered and removed and the tags appeared to get updated, that is, `cellForRowAtIndexPath` (where I set the tag) was called again for the rows affected. I will do some more extensive tests... – mluisbrown Jul 31 '13 at 14:29
  • 1
    Example - if you insert a single row at an index path above at least some of the visible rows on the screen, the visible rows below the insertion point are not updated and will still have the old tags. This assumes you don't call `reloadData`, just `insertRowsAtIndexPaths:`. If you simply reorder a visible cell from one part of the screen to another part of the visible screen so no cells enter or leave the screen, `cellForRowAtIndexPath:` is not called at all. Again, this assumes you are not calling `reloadData`. – rmaddy Jul 31 '13 at 16:15
  • Ok, makes sense. I've adopted your method above which works flawlessly. – mluisbrown Jul 31 '13 at 22:06
  • 1
    @maddy btw, `self.bounds.origin` in your example code should be `field.bounds.origin`. I tried to edit your answer but SO wouldn't let me make such a small change. – mluisbrown Jul 31 '13 at 22:12
  • I tried this and I dont understand why it gives me pressed button +1 row. – Geekoder Mar 12 '14 at 19:00
  • How about get cell from method 'viewDidLoad'?? can you anyone explain me? – LKM Mar 19 '15 at 07:31
12

One slightly better way of doing it is to iterate up through the view hierarchy, checking for each superview if it's an UITableViewCell using the class method. That way you are not constrained by the number of superviews between your UITextField and the cell.

Something along the lines of:

UIView *view = field;
while (view && ![view isKindOfClass:[UITableViewCell class]]){ 
    view = view.superview;
}
Cezar
  • 55,636
  • 19
  • 86
  • 87
  • Really excellent and brilliant ! many thanks, you saved me lot of time. Just don't forget to cast "view" later to "UITableViewCell*" and it works like a charm, and is nicely compact. (But why didn't I think of it all by myself? ;-) ) – Chrysotribax Mar 22 '17 at 23:40
3

You can attach the UITableViewCell itself as a weak association to the UITextField, then pluck it out in the UITextFieldDelegate method.

const char kTableViewCellAssociatedObjectKey;

In your UITableViewCell subclass:

- (void)awakeFromNib {
    [super awakeFromNib];
    objc_setAssociatedObject(textField, &kTableViewCellAssociatedObjectKey, OBJC_ASSOCIATION_ASSIGN);
}

In your UITextFieldDelegate method:

UITableViewCell *cell = objc_getAssociatedObject(textField, &kTableViewCellAssociatedObjectKey);
NSIndexPath *indexPath = [self.tableView indexPathForCell:cell];
//...

I'd also recommend re-associating every time a cell is dequeued from the UITableView to ensure that the text field is associated with the correct cell.

Jacob Relkin
  • 161,348
  • 33
  • 346
  • 320
1

Basically in this case, I would prefer you to put the IBAction method into cell instead of view controller. And then when an action is triggered, a cell send a delegate to a view controller instance.

Here is an example:

@protocol MyCellDelegate;


@interface MyCell : UITableViewCell

@property (nonatomic, weak) id<MyCellDelegate> delegate;

@end

@protocol MyCellDelegate <NSObject>

- (void)tableViewCell:(MyCell *)cell textFieldDidFinishEditingWithText:(NSString *)text;

@end

In a implementation of a cell:

- (IBAction)itemFinishedEditing:(UITextField *)sender
{
    // You may check respondToSelector first
    [self.delegate tableViewCell:self textFieldDidFinishEditingWithText:sender.text];
}

So now a cell will pass itself and the text via the delegate method.

Suppose a view controller has set the delegate of a cell to self. Now a view controller will implement a delegate method.

In the implementation of your view controller:

- (void)tableViewCell:(MyCell *)cell textFieldDidFinishEditingWithText:(NSString *)text
{
    NSIndexPath *indexPath = [_tableView indexPathForCell:cell];
    _list.items[indexPath.row] = text;
}

This approach will also work no matter how Apple will change a view hierarchy of a table view cell.

Oscar
  • 651
  • 5
  • 13
  • 1
    I think this may be a violation of MVC principles. Anyone else think so? – Jacob Relkin Jul 20 '13 at 02:02
  • 1
    @JacobRelkin I agree. It gives the cell a responsibility that belongs to a controller. – Cezar Jul 20 '13 at 02:06
  • This approach (creating a custom delegate protocol for your `UITableViewCell`) is exactly what is suggested in the Ray Wenderlich [tutorial](http://www.raywenderlich.com/21842/how-to-make-a-gesture-driven-to-do-list-app-part-13) for making a 'Clear' style todo list app (search for SHCTableViewCellDelegate to get to the right section). I'm not sure that it does break MVC. The cell is merely informing the controller that something happened and it's up to the controller to do what it sees fit as a result (eg. alter the model). – mluisbrown Aug 07 '13 at 11:13