[ios] How to get the indexpath.row when an element is activated?

I have a tableview with buttons and I want to use the indexpath.row when one of them is tapped. This is what I currently have, but it always is 0

var point = Int()
func buttonPressed(sender: AnyObject) {
    let pointInTable: CGPoint =         sender.convertPoint(sender.bounds.origin, toView: self.tableView)
    let cellIndexPath = self.tableView.indexPathForRowAtPoint(pointInTable)
    println(cellIndexPath)
    point = cellIndexPath!.row
    println(point)
}

This question is related to ios swift iphone uitableview

The answer is


In Swift 4 , just use this:

func buttonTapped(_ sender: UIButton) {
        let buttonPostion = sender.convert(sender.bounds.origin, to: tableView)

        if let indexPath = tableView.indexPathForRow(at: buttonPostion) {
            let rowIndex =  indexPath.row
        }
}

UPDATE: Getting the indexPath of the cell containing the button (both section and row):

Using Button Position

Inside of your buttonTapped method, you can grab the button's position, convert it to a coordinate in the tableView, then get the indexPath of the row at that coordinate.

func buttonTapped(_ sender:AnyObject) {
    let buttonPosition:CGPoint = sender.convert(CGPoint.zero, to:self.tableView)
    let indexPath = self.tableView.indexPathForRow(at: buttonPosition)
}

NOTE: Sometimes you can run into an edge case when using the function view.convert(CGPointZero, to:self.tableView) results in finding nil for a row at a point, even though there is a tableView cell there. To fix this, try passing a real coordinate that is slightly offset from the origin, such as:

let buttonPosition:CGPoint = sender.convert(CGPoint.init(x: 5.0, y: 5.0), to:self.tableView)

Previous Answer: Using Tag Property (only returns row)

Rather than climbing into the superview trees to grab a pointer to the cell that holds the UIButton, there is a safer, more repeatable technique utilizing the button.tag property mentioned by Antonio above, described in this answer, and shown below:

In cellForRowAtIndexPath: you set the tag property:

button.tag = indexPath.row
button.addTarget(self, action: "buttonClicked:", forControlEvents: UIControlEvents.TouchUpInside)

Then, in the buttonClicked: function, you reference that tag to grab the row of the indexPath where the button is located:

func buttonClicked(sender:UIButton) {
    let buttonRow = sender.tag
}

I prefer this method since I've found that swinging in the superview trees can be a risky way to design an app. Also, for objective-C I've used this technique in the past and have been happy with the result.


I used convertPoint method to get point from tableview and pass this point to indexPathForRowAtPoint method to get indexPath

 @IBAction func newsButtonAction(sender: UIButton) {
        let buttonPosition = sender.convertPoint(CGPointZero, toView: self.newsTableView)
        let indexPath = self.newsTableView.indexPathForRowAtPoint(buttonPosition)
        if indexPath != nil {
            if indexPath?.row == 1{
                self.performSegueWithIdentifier("alertViewController", sender: self);
            }   
        }
    }

Try using #selector to call the IBaction.In the cellforrowatindexpath

            cell.editButton.tag = indexPath.row
        cell.editButton.addTarget(self, action: #selector(editButtonPressed), for: .touchUpInside)

This way you can access the indexpath inside the method editButtonPressed

func editButtonPressed(_ sender: UIButton) {

print(sender.tag)//this value will be same as indexpath.row

}

For Swift2.1

I found a way to do it, hopefully, it'll help.

let point = tableView.convertPoint(CGPoint.zero, fromView: sender)

    guard let indexPath = tableView.indexPathForRowAtPoint(point) else {
        fatalError("can't find point in tableView")
    }

In Swift 3. Also used guard statements, avoiding a long chain of braces.

func buttonTapped(sender: UIButton) {
    guard let cellInAction = sender.superview as? UITableViewCell else { return }
    guard let indexPath = tableView?.indexPath(for: cellInAction) else { return }

    print(indexPath)
}

Sometimes button may be inside of another view of UITableViewCell. In that case superview.superview may not give the cell object and hence the indexPath will be nil.

In that case we should keep finding the superview until we get the cell object.

Function to get cell object by superview

func getCellForView(view:UIView) -> UITableViewCell?
{
    var superView = view.superview

    while superView != nil
    {
        if superView is UITableViewCell
        {
            return superView as? UITableViewCell
        }
        else
        {
            superView = superView?.superview
        }
    }

    return nil
}

Now we can get indexPath on button tap as below

@IBAction func tapButton(_ sender: UIButton)
{
    let cell = getCellForView(view: sender)
    let indexPath = myTabelView.indexPath(for: cell)
}

USING A SINGLE TAG FOR ROWS AND SECTIONS

There is a simple way to use tags for transmitting the row/item AND the section of a TableView/CollectionView at the same time.

Encode the IndexPath for your UIView.tag in cellForRowAtIndexPath :

buttonForCell.tag = convertIndexPathToTag(with: indexPath)

Decode the IndexPath from your sender in your target selector:

    @IBAction func touchUpInsideButton(sender: UIButton, forEvent event: UIEvent) {

        var indexPathForButton = convertTagToIndexPath(from: sender)

    }

Encoder and Decoder:

func convertIndexPathToTag(indexPath: IndexPath) -> Int {
    var tag: Int = indexPath.row + (1_000_000 * indexPath.section)
    
    return tag
}

func convertTagToIndexPath(from sender: UIButton) -> IndexPath {
    var section: Int = Int((Float(sender.tag) / 1_000_000).rounded(.down))
    var row: Int = sender.tag - (1_000_000 * section)

    return IndexPath(row: row, section: section)
}

Provided that you don’t need more than 4294967296 rows/items on a 32bit device ;-) e.g.

  • 42949 sections with 100_000 items/rows
  • 4294 sections with 1_000_000 items/rows - (like in the example above)
  • 429 sections with 10_000_000 items/rows

—-

WARNING: Keep in mind that when deleting or inserting rows/items in your TableView/CollectionView you have to reload all rows/items after your insertion/deletion point in order to keep the tag numbers of your buttons in sync with your model.

—-


// CustomCell.swift

protocol CustomCellDelegate {
    func tapDeleteButton(at cell: CustomCell)
}

class CustomCell: UICollectionViewCell {
    
    var delegate: CustomCellDelegate?
    
    fileprivate let deleteButton: UIButton = {
        let button = UIButton(frame: .zero)
        button.setImage(UIImage(named: "delete"), for: .normal)
        button.addTarget(self, action: #selector(deleteButtonTapped(_:)), for: .touchUpInside)
        button.translatesAutoresizingMaskIntoConstraints = false
        return button
    }()
    
    @objc fileprivate func deleteButtonTapped(_sender: UIButton) {
        delegate?.tapDeleteButton(at: self)
    }
    
}

//  ViewController.swift

extension ViewController: UICollectionViewDataSource {

    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: customCellIdentifier, for: indexPath) as? CustomCell else {
            fatalError("Unexpected cell instead of CustomCell")
        }
        cell.delegate = self
        return cell
    }

}

extension ViewController: CustomCellDelegate {

    func tapDeleteButton(at cell: CustomCell) {
        // Here we get the indexPath of the cell what we tapped on.
        let indexPath = collectionView.indexPath(for: cell)
    }

}

Use an extension to UITableView to fetch the cell that contains any view:


@Paulw11's answer of setting up a custom cell type with a delegate property that sends messages to the table view is a good way to go, but it requires a certain amount of work to set up.

I think walking the table view cell's view hierarchy looking for the cell is a bad idea. It is fragile - if you later enclose your button in a view for layout purposes, that code is likely to break.

Using view tags is also fragile. You have to remember to set up the tags when you create the cell, and if you use that approach in a view controller that uses view tags for another purpose you can have duplicate tag numbers and your code can fail to work as expected.

I have created an extension to UITableView that lets you get the indexPath for any view that is contained in a table view cell. It returns an Optional that will be nil if the view passed in actually does not fall within a table view cell. Below is the extension source file in it's entirety. You can simply put this file in your project and then use the included indexPathForView(_:) method to find the indexPath that contains any view.

//
//  UITableView+indexPathForView.swift
//  TableViewExtension
//
//  Created by Duncan Champney on 12/23/16.
//  Copyright © 2016-2017 Duncan Champney.
//  May be used freely in for any purpose as long as this 
//  copyright notice is included.

import UIKit

public extension UITableView {
  
  /**
  This method returns the indexPath of the cell that contains the specified view
   
   - Parameter view: The view to find.
   
   - Returns: The indexPath of the cell containing the view, or nil if it can't be found
   
  */
  
    func indexPathForView(_ view: UIView) -> IndexPath? {
        let center = view.center
        let viewCenter = self.convert(center, from: view.superview)
        let indexPath = self.indexPathForRow(at: viewCenter)
        return indexPath
    }
}

To use it, you can simply call the method in the IBAction for a button that's contained in a cell:

func buttonTapped(_ button: UIButton) {
  if let indexPath = self.tableView.indexPathForView(button) {
    print("Button tapped at indexPath \(indexPath)")
  }
  else {
    print("Button indexPath not found")
  }
}

(Note that the indexPathForView(_:) function will only work if the view object it's passed is contained by a cell that's currently on-screen. That's reasonable, since a view that is not on-screen doesn't actually belong to a specific indexPath; it's likely to be assigned to a different indexPath when it's containing cell is recycled.)

EDIT:

You can download a working demo project that uses the above extension from Github: TableViewExtension.git


I found a very easy way to use for manage any cell in tableView and collectionView by using a Model class and this a work as perfectly.

There is indeed a much better way to handle this now. This will work for manage cell and value.

Here is my output(screenshot) so see this:

enter image description here

  1. Its very simple to create model class, please follow the below procedure. Create swift class with name RNCheckedModel, write the code as below.
class RNCheckedModel: NSObject {

    var is_check = false
    var user_name = ""

    }
  1. create your cell class
class InviteCell: UITableViewCell {

    @IBOutlet var imgProfileImage: UIImageView!
    @IBOutlet var btnCheck: UIButton!
    @IBOutlet var lblName: UILabel!
    @IBOutlet var lblEmail: UILabel!
    }
  1. and finally use model class in your UIViewController when you use your UITableView.
    class RNInviteVC: UIViewController, UITableViewDelegate, UITableViewDataSource {


    @IBOutlet var inviteTableView: UITableView!
    @IBOutlet var btnInvite: UIButton!

    var checkArray : NSMutableArray = NSMutableArray()
    var userName : NSMutableArray = NSMutableArray()

    override func viewDidLoad() {
        super.viewDidLoad()
        btnInvite.layer.borderWidth = 1.5
        btnInvite.layer.cornerRadius = btnInvite.frame.height / 2
        btnInvite.layer.borderColor =  hexColor(hex: "#512DA8").cgColor

        var userName1 =["Olivia","Amelia","Emily","Isla","Ava","Lily","Sophia","Ella","Jessica","Mia","Grace","Evie","Sophie","Poppy","Isabella","Charlotte","Freya","Ruby","Daisy","Alice"]


        self.userName.removeAllObjects()
        for items in userName1 {
           print(items)


            let model = RNCheckedModel()
            model.user_name = items
            model.is_check = false
            self.userName.add(model)
        }
      }
     @IBAction func btnInviteClick(_ sender: Any) {

    }
       func tableView(_ tableView: UITableView, numberOfRowsInSection 
       section: Int) -> Int {
        return userName.count
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell: InviteCell = inviteTableView.dequeueReusableCell(withIdentifier: "InviteCell", for: indexPath) as! InviteCell

        let image = UIImage(named: "ic_unchecked")
        cell.imgProfileImage.layer.borderWidth = 1.0
        cell.imgProfileImage.layer.masksToBounds = false
        cell.imgProfileImage.layer.borderColor = UIColor.white.cgColor
        cell.imgProfileImage.layer.cornerRadius =  cell.imgProfileImage.frame.size.width / 2
        cell.imgProfileImage.clipsToBounds = true

        let model = self.userName[indexPath.row] as! RNCheckedModel
        cell.lblName.text = model.user_name

        if (model.is_check) {
            cell.btnCheck.setImage(UIImage(named: "ic_checked"), for: UIControlState.normal)
        }
        else {
            cell.btnCheck.setImage(UIImage(named: "ic_unchecked"), for: UIControlState.normal)
        }

        cell.btnCheck.tag = indexPath.row
        cell.btnCheck.addTarget(self, action: #selector(self.btnCheck(_:)), for: .touchUpInside)

        cell.btnCheck.isUserInteractionEnabled = true

    return cell

    }

    func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        return 80

    }

    @objc func btnCheck(_ sender: UIButton) {

        let tag = sender.tag
        let indexPath = IndexPath(row: tag, section: 0)
        let cell: InviteCell = inviteTableView.dequeueReusableCell(withIdentifier: "InviteCell", for: indexPath) as! InviteCell

        let model = self.userName[indexPath.row] as! RNCheckedModel

        if (model.is_check) {

            model.is_check = false
            cell.btnCheck.setImage(UIImage(named: "ic_unchecked"), for: UIControlState.normal)

            checkArray.remove(model.user_name)
            if checkArray.count > 0 {
                btnInvite.setTitle("Invite (\(checkArray.count))", for: .normal)
                print(checkArray.count)
                UIView.performWithoutAnimation {
                    self.view.layoutIfNeeded()
                }
            } else {
                btnInvite.setTitle("Invite", for: .normal)
                UIView.performWithoutAnimation {
                    self.view.layoutIfNeeded()
                }
            }

        }else {

            model.is_check = true
            cell.btnCheck.setImage(UIImage(named: "ic_checked"), for: UIControlState.normal)

            checkArray.add(model.user_name)
            if checkArray.count > 0 {
                btnInvite.setTitle("Invite (\(checkArray.count))", for: .normal)
                UIView.performWithoutAnimation {
                self.view.layoutIfNeeded()
                }
            } else {
                 btnInvite.setTitle("Invite", for: .normal)
            }
        }

        self.inviteTableView.reloadData()
    }

    func hexColor(hex:String) -> UIColor {
        var cString:String = hex.trimmingCharacters(in: .whitespacesAndNewlines).uppercased()

        if (cString.hasPrefix("#")) {
            cString.remove(at: cString.startIndex)
        }

        if ((cString.count) != 6) {
            return UIColor.gray
        }

        var rgbValue:UInt32 = 0
        Scanner(string: cString).scanHexInt32(&rgbValue)

        return UIColor(
            red: CGFloat((rgbValue & 0xFF0000) >> 16) / 255.0,
            green: CGFloat((rgbValue & 0x00FF00) >> 8) / 255.0,
            blue: CGFloat(rgbValue & 0x0000FF) / 255.0,
            alpha: CGFloat(1.0)
        )
    }
    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()

    }

     }

My approach to this sort of problem is to use a delegate protocol between the cell and the tableview. This allows you to keep the button handler in the cell subclass, which enables you to assign the touch up action handler to the prototype cell in Interface Builder, while still keeping the button handler logic in the view controller.

It also avoids the potentially fragile approach of navigating the view hierarchy or the use of the tag property, which has issues when cells indexes change (as a result of insertion, deletion or reordering)

CellSubclass.swift

protocol CellSubclassDelegate: class {
    func buttonTapped(cell: CellSubclass)
}

class CellSubclass: UITableViewCell {

@IBOutlet var someButton: UIButton!

weak var delegate: CellSubclassDelegate?

override func prepareForReuse() {
    super.prepareForReuse()
    self.delegate = nil
}

@IBAction func someButtonTapped(sender: UIButton) {
    self.delegate?.buttonTapped(self)
}

ViewController.swift

class MyViewController: UIViewController, CellSubclassDelegate {

    @IBOutlet var tableview: UITableView!

    func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {

        let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as! CellSubclass

        cell.delegate = self

        // Other cell setup

    } 

    //  MARK: CellSubclassDelegate

    func buttonTapped(cell: CellSubclass) {
        guard let indexPath = self.tableView.indexPathForCell(cell) else {
            // Note, this shouldn't happen - how did the user tap on a button that wasn't on screen?
            return
        }

        //  Do whatever you need to do with the indexPath

        print("Button tapped on row \(indexPath.row)")
    }
} 

In my case i have multiple sections and both the section and row index is vital, so in such a case i just created a property on UIButton which i set the cell indexPath like so:

fileprivate struct AssociatedKeys {
    static var index = 0
}

extension UIButton {

    var indexPath: IndexPath? {
        get {
            return objc_getAssociatedObject(self, &AssociatedKeys.index) as? IndexPath
        }
        set {
            objc_setAssociatedObject(self, &AssociatedKeys.index, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
        }
    }
}

Then set the property in cellForRowAt like this :

func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCellWithIdentifier("Cell") as! Cell
    cell.button.indexPath = indexPath
}

Then in the handleTapAction you can get the indexPath like this :

@objc func handleTapAction(_ sender: UIButton) {
    self.selectedIndex = sender.indexPath

}

Swift 4 and 5

Method 1 using Protocol delegate

For example, you have a UITableViewCell with name MyCell

class MyCell: UITableViewCell {
    
    var delegate:MyCellDelegate!
    
    @IBAction private func myAction(_ sender: UIButton){
        delegate.didPressButton(cell: self)
    }
}

Now create a protocol

protocol MyCellDelegate {
    func didPressButton(cell: UITableViewCell)
}

Next step, create an Extension of UITableView

extension UITableView {
    func returnIndexPath(cell: UITableViewCell) -> IndexPath?{
        guard let indexPath = self.indexPath(for: cell) else {
            return nil
        }
        return indexPath
    }
}

In your UIViewController implement the protocol MyCellDelegate

class ViewController: UIViewController, MyCellDelegate {
     
    func didPressButton(cell: UITableViewCell) {
        if let indexpath = self.myTableView.returnIndexPath(cell: cell) {
              print(indexpath)
        }
    }
}

Method 2 using closures

In UIViewController

override func viewDidLoad() {
        super.viewDidLoad()
       //using the same `UITableView extension` get the IndexPath here
        didPressButton = { cell in
            if let indexpath = self.myTableView.returnIndexPath(cell: cell) {
                  print(indexpath)
            }
        }
    }
 var didPressButton: ((UITableViewCell) -> Void)

class MyCell: UITableViewCell {

    @IBAction private func myAction(_ sender: UIButton){
        didPressButton(self)
    }
}

Note:- if you want to get UICollectionView indexPath you can use this UICollectionView extension and repeat the above steps

extension UICollectionView {
    func returnIndexPath(cell: UICollectionViewCell) -> IndexPath?{
        guard let indexPath = self.indexPath(for: cell) else {
            return nil
        }
        return indexPath
    }
}

After seeing Paulw11's suggestion of using a delegate callback, I wanted to elaborate on it slightly/put forward another, similar suggestion. Should you not want to use the delegate pattern you can utilise closures in swift as follows:

Your cell class:

class Cell: UITableViewCell {
    @IBOutlet var button: UIButton!

    var buttonAction: ((sender: AnyObject) -> Void)?

    @IBAction func buttonPressed(sender: AnyObject) {
        self.buttonAction?(sender)
    }
}

Your cellForRowAtIndexPath method:

func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCellWithIdentifier("Cell") as! Cell
    cell.buttonAction = { (sender) in
        // Do whatever you want from your button here.
    }
    // OR
    cell.buttonAction = buttonPressed // <- Method on the view controller to handle button presses.
}

Extend UITableView to create function that get indexpath for a view:

extension UITableView {
    func indexPath(for view: UIView) -> IndexPath? {
        self.indexPathForRow(at: view.convert(.zero, to: self))
    }
}

How to use:

let row = tableView.indexPath(for: sender)?.row

Solution:

You have a button (myButton) or any other view in cell. Assign tag in cellForRowAt like this

cell.myButton.tag = indexPath.row

Now in you tapFunction or any other. Fetch it out like this and save it in a local variable.

currentCellNumber = (sender.view?.tag)!

After this you can use anywhere this currentCellNumber to get the indexPath.row of selected button.

Enjoy!


Very Simple getting Index Path swift 4, 5

let cell = tableView.dequeueReusableCellWithIdentifier("Cell") as! Cell
cell.btn.tag = indexPath.row
cell.btn.addTarget(self, action: "buttonTapped:", forControlEvents: 
UIControlEvents.TouchUpInside)

How to get IndexPath Inside Btn Click :

func buttonTapped(_ sender: UIButton) {
     print(sender.tag)  
}

Since the sender of the event handler is the button itself, I'd use the button's tag property to store the index, initialized in cellForRowAtIndexPath.

But with a little more work I'd do in a completely different way. If you are using a custom cell, this is how I would approach the problem:

  • add an 'indexPath` property to the custom table cell
  • initialize it in cellForRowAtIndexPath
  • move the tap handler from the view controller to the cell implementation
  • use the delegation pattern to notify the view controller about the tap event, passing the index path

Examples related to ios

Adding a UISegmentedControl to UITableView Crop image to specified size and picture location Undefined Symbols error when integrating Apptentive iOS SDK via Cocoapods Keep placeholder text in UITextField on input in IOS Accessing AppDelegate from framework? Autoresize View When SubViews are Added Warp \ bend effect on a UIView? Speech input for visually impaired users without the need to tap the screen make UITableViewCell selectable only while editing Xcode 12, building for iOS Simulator, but linking in object file built for iOS, for architecture arm64

Examples related to swift

Make a VStack fill the width of the screen in SwiftUI Xcode 10.2.1 Command PhaseScriptExecution failed with a nonzero exit code Command CompileSwift failed with a nonzero exit code in Xcode 10 Convert Json string to Json object in Swift 4 iOS Swift - Get the Current Local Time and Date Timestamp Xcode 9 Swift Language Version (SWIFT_VERSION) How do I use Safe Area Layout programmatically? How can I use String substring in Swift 4? 'substring(to:)' is deprecated: Please use String slicing subscript with a 'partial range from' operator Safe Area of Xcode 9 The use of Swift 3 @objc inference in Swift 4 mode is deprecated?

Examples related to iphone

Detect if the device is iPhone X Xcode 8 shows error that provisioning profile doesn't include signing certificate Access files in /var/mobile/Containers/Data/Application without jailbreaking iPhone Certificate has either expired or has been revoked Missing Compliance in Status when I add built for internal testing in Test Flight.How to solve? cordova run with ios error .. Error code 65 for command: xcodebuild with args: "Could not find Developer Disk Image" Reason: no suitable image found iPad Multitasking support requires these orientations How to insert new cell into UITableView in Swift

Examples related to uitableview

Adding a UISegmentedControl to UITableView make UITableViewCell selectable only while editing How to fix Error: this class is not key value coding-compliant for the key tableView.' UITableView example for Swift Swift - how to make custom header for UITableView? How to insert new cell into UITableView in Swift How to detect tableView cell touched or clicked in swift Dynamic Height Issue for UITableView Cells (Swift) UIButton action in table view cell How to get the indexpath.row when an element is activated?