39

I have a UITableViewCell with UISwitch as accessoryview of each cell. When I change the value of the switch in a cell, how can I know in which row the switch is? I need the row number in the switch value changed event.

Bhavin Ramani
  • 3,221
  • 5
  • 30
  • 41
Jean Paul
  • 2,389
  • 5
  • 27
  • 37
  • You could subclass the switch, add a property "index" and at creation time of the cell (and the switch) set this property to the current index of the cell. By pressing the switch you can read this property and thats your cell-index. – TRD Feb 14 '12 at 09:37
  • @TRD You would have to update the index property every time you return the cell from `tableView:cellForRowAtIndexPath:`, because a cell can be reused for different rows. – rob mayoff Feb 14 '12 at 09:56
  • check this link:http://stackoverflow.com/a/2562367/845115 – Mudit Bajpai Feb 14 '12 at 10:05

10 Answers10

160

Tags, subclasses, or view hierarchy navigation are too much work!. Do this in your action method:

CGPoint hitPoint = [sender convertPoint:CGPointZero toView:self.tableView]; 
NSIndexPath *hitIndex = [self.tableView indexPathForRowAtPoint:hitPoint];

Works with any type of view, multi section tables, whatever you can throw at it - as long as the origin of your sender is within the cell's frame (thanks rob!), which will usually be the case.

And here it is in a UITableView Swift extension:

extension UITableView {
    func indexPath(for view: UIView) -> IndexPath? {
        let location = view.convert(CGPoint.zero, to: self)
        return self.indexPathForRow(at: location)
    }
}
jrturton
  • 118,105
  • 32
  • 252
  • 268
  • 3
    @GajendraKChauhan - like it says in the answer, in your action method. By this I mean whichever method is called when the button in your cell is tapped, where the button itself is passed as `sender` – jrturton Jun 25 '13 at 08:19
  • @Zac24 the sender is whatever button or control is linked to the action method. This code doesn't go into cellForRowAtIndexPath, it goes into an action method from a button or switch or whatever that is added to a cell. – jrturton Oct 14 '13 at 08:58
  • I am trying to add Swift extension and I'm getting, "Declaration is only valid at file scope." Is there something I'm missing? – deebs Dec 02 '14 at 15:55
  • 1
    @deebs You have to add the extension at the top level in a file, e.g. not inside another class or anything – jrturton Dec 02 '14 at 16:11
  • I get the error: `terminating with uncaught exception of type NSException` I know that I am not using the swift extension right. If someone can contribute to extend this answer and add how to use it, please do. – Jesus Rodriguez Jan 29 '16 at 02:00
  • Can we use `[sender center]` instead of `CGPointZero`? Also don't forget to involve any subviews, the `sender` might be wrapped in, in your `convertTo:` (in my case the button is in a slidable UIView of a smaller dimension than the parent cell). – David Jirman Apr 06 '16 at 08:57
  • 1
    @DavidJirman Center is in the wrong coordinate space, and the view hierarchy is already taken into account by the convert method. – jrturton Apr 06 '16 at 08:59
  • @jrturton Got it, eventually i was doing the same but in a more complicated fashion. Thanks for the answer! – David Jirman Apr 06 '16 at 09:55
  • I think this answer is acceptable, but if we're really measuring "too much work" then a few rounds of a while loop walking up the superview list is the winning approach. Consider the number of comparisons happening in a hitTest (of the table cell's whole hierarchy), not to mention the original coordinate space conversion. Not sure why some on SO are opposed to looping superviews. It's safe now and in the future. – danh Jan 10 '17 at 15:43
  • @danh this isn't hit testing, too much work referred to the amount of code, and supervise walking isn't safe since the hierarchy changes with each iOS release. – jrturton Jan 10 '17 at 15:58
  • About work: finding a leaf view in a hierarchy under a CGPoint is a hit test. I suspect, upon finding the subview, the table view's indexPathAtPoint code then walks up superviews until it finds the cell. About future safety: Do you anticipate a release - ever - when views within tableview cells will cease to be descendants of tableview cells? It's almost tautological. – danh Jan 10 '17 at 16:15
  • I just noticed your discussion here with @robmayoff, and his answer. So you've been over this territory before. Anyway, I agree that apple could have fixed this with clearly named tableview method and has left ambiguous how apps are supposed to handle this common problem. – danh Jan 10 '17 at 16:23
  • @danh yes, proper SDK support would be lovely. The main driver for my answer was from seeing too much code that just went`cell = button.superview.superview`, and that tags are useless and need to die in a fire. – jrturton Jan 10 '17 at 16:45
5

If you set the tag property to the row number (as suggested by other answers), you have to update it every time in tableView:cellForRowAtIndexPath: (because a cell can be reused for different rows).

Instead, when you need the row number, you can walk up the superview chain from the UISwitch (or any other view) to the UITableViewCell, and then to the UITableView, and ask the table view for the index path of the cell:

static NSIndexPath *indexPathForView(UIView *view) {
    while (view && ![view isKindOfClass:[UITableViewCell class]])
        view = view.superview;
    if (!view)
        return nil;
    UITableViewCell *cell = (UITableViewCell *)view;
    while (view && ![view isKindOfClass:[UITableView class]])
        view = view.superview;
    if (!view)
        return nil;
    UITableView *tableView = (UITableView *)view;
    return [tableView indexPathForCell:cell];
}

This doesn't require anything in tableView:cellForRowAtIndexPath:.

rob mayoff
  • 375,296
  • 67
  • 796
  • 848
  • I agree about tags, but your solution seems a little convoluted. Can you see any drawbacks in the method I am proposing? There seem to be so many ugly hacks around to solve this common problem and I'm pretty pleased with mine, but I don't see it used anywhere else! – jrturton Feb 14 '12 at 10:06
  • @jrturton That is an interesting approach. It will fail if the `UISwitch`'s origin happens to be outside of its cell, which is admittedly unlikely. – rob mayoff Feb 14 '12 at 10:13
  • Yes, it would be hard to tap in that case! – jrturton Feb 14 '12 at 10:18
  • If the origin is at (-1,0), most of it is still visible and touchable. – rob mayoff Feb 14 '12 at 10:26
  • I think the code here puts off potential buyers with the second loop that hunts for the table view. The caller better know the table view when he calls, otherwise he'll be stuck wondering to which table he should apply the resulting path. This is a perfectly good tableview extension. I think the approach has been wrongly discredited around here as unsafe relative to SDK changes. It's as safe as anything in UIKit, IMO -- and (probably, though probably trivially so who cares) less computation than the accepted solution. (I added an answer to express my POV) – danh Jan 10 '17 at 16:43
3

Accepted solution is a clever hack.

However why do we need to use hitpoint if we can utilize already available tag property on UIView? You would say that tag can store only either row or section since its a single Int.

Well... Don't forget your roots guys (CS101). A single Int can store two twice-smaller size integers. And here is an extension for this:

extension Int {

    public init(indexPath: IndexPath) {
        var marshalledInt: UInt32 = 0xffffffff

        let rowPiece = UInt16(indexPath.row)
        let sectionPiece = UInt16(indexPath.section)
        marshalledInt = marshalledInt & (UInt32(rowPiece) << 16)
        marshalledInt = marshalledInt + UInt32(sectionPiece)

        self.init(bitPattern: UInt(marshalledInt))
    }

    var indexPathRepresentation: IndexPath {
        let section = self & 0x0000ffff

        let pattern: UInt32 = 0xffff0000
        let row = (UInt32(self) & pattern) >> 16
        return IndexPath(row: Int(row), section: Int(section))
    }
}

In your tableView(_:, cellForRowAt:) you can then:

cell.yourSwitch.tag = Int(indexPath: indexPath)

And then in the action handler you would can:

func didToogle(sender: UISwitch){
    print(sender.tag.indexPathRepresentation)
}

However please note it's limitation: row and section need to be not larger then 65535. (UInt16.max)

I doubt your tableView's indexes will go that high but in case they do, challenge yourself and implement more efficient packing scheme. Say if we have a section very small, we don't need all 16 bits to represent a section. We can have our int layout like:

{section area length}{all remaining}[4 BITS: section area length - 1]

that is our 4 LSBs indicate the length of section area - 1, given that we allocate at least 1 bit for a section. Thus in case of our section is 0, the row can occupy up to 27 bits ([1][27][4]), which definitely should be enough.

ambientlight
  • 7,212
  • 3
  • 49
  • 61
3

in cellForRowAtIndexPath:, set the tag property of your control to indexPath.row

wattson12
  • 11,176
  • 2
  • 32
  • 34
2

I prefer using subviews, if you know your layout it's generally super simple and 1 line short...

    NSIndexPath *indexPath = [tableView indexPathForCell:(UITableViewCell *)[[sender superview] superview]];

Thats it, if its more nested, add in more superviews.

Bit more info:

all you are doing is asking for the parent view and its parent view which is the cell. Then you are asking your tableview for the indexpath of that cell you just got.

Andres Canella
  • 3,706
  • 1
  • 35
  • 47
  • This is by far the best solution around. I have seen other solutions fails when recycling cells – Alejandro Luengo Aug 08 '13 at 21:29
  • Under XCode 5 / SDK7 using interface Builder to create cells (as "prototypes") you run into a problem that on iOS6 and iOS7 devices an embedded control is nested at different subview levels. Seriously. I was doing it this way but had to change to the "hit position" hack. – jimkberry Oct 08 '13 at 21:04
1

The accepted answer on this post is perfectly fine. I'd like to suggest to readers that the following, derived from @robmayoff on this post, is also perfectly fine:

- (NSIndexPath *)indexPathForView:(UIView *)view inTableView:(UITableView *)tableView {
    while (view && ![view isKindOfClass:[UITableViewCell class]])
        view = view.superview;
    UITableViewCell *cell = (UITableViewCell *)view;
    return [tableView indexPathForCell:cell];
}

Some have asserted that this approach contains too much computational work because of the while loop. The alternative, convert the view's origin to table view coordinate space and call indexPathForRowAtPoint:, hides even more work.

Some have asserted that this approach is unsafe relative to potential SDK changes. In fact, Apple has already changed the tableview cell hierarchy once, adding a contentView to the cell. This approach works before and after such a change. As long as view ancestors can be found via a chain of superviews (which is as fundamental as anything in UIKit), this is good code.

danh
  • 62,181
  • 10
  • 95
  • 136
1

One common way to do this is to set the tag of the control (in your case the switch) to something that can be used to identify the row or represented object.

For example, in tableView:cellForRowAtIndexPath: set the tag property of the switch to the indexPath.row and in your action method you can get the tag from the sender.

Personally, I don't like this approach and prefer subclassing UITableViewCell. Also, it may be a good idea to add an "offset" to the tag to prevent any conflicts with the tags of other views.

Daniel Rinser
  • 8,855
  • 4
  • 41
  • 39
0

A colleague suggested the following, which I made into a UITableView category:

+(UITableViewCell*)findParentCellForSubview:(UIView*)view
{
    while (([view isKindOfClass:[UITableViewCell class]] == NO) && ([view superview] != nil))
        view = [view superview];

    if ([view superview] != nil)
        return (UITableViewCell*)view;

    return nil;
}

Still hackly - but it works.

jimkberry
  • 1,317
  • 7
  • 8
0

One more variant of using superView. Works like category for UIView.

- (UITableViewCell *)superCell
{
    if (!self.superview) {
        return nil;
    }

    if ([self.superview isKindOfClass:[UITableViewCell class]]) {
        return (UITableViewCell *)self.superview;
    }

    return [self.superview superCell];
}
serj
  • 301
  • 4
  • 10
0

i dont know about the multiple sections but i can give you for the one section...

-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
NSInteger index=indexPath.row;
NSString *string=[[NSString alloc]initWithFormat:@"%ld",(long)index];
}

from this you can get the row number and you can save it to the string....

Smit
  • 1