Issue Detecting Button cellForRowAt
The easiest and most efficient way in Swift is a callback closure.
- Subclass
UITableViewCell
, the viewWithTag way to identify UI elements is outdated. Set the class of the custom cell to the name of the subclass and set the identifier to
ButtonCellIdentifier
in Interface Builder.Add a
callback
property.Add an action and connect the button to the action.
class ButtonCell: UITableViewCell {
var callback : (() -> Void)?
@IBAction func buttonPressed(_ sender : UIButton) {
callback?()
}
}In
cellForRow
assign the callback to the custom cell.override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "ButtonCellIdentifier", for: indexPath) as! ButtonCell
cell.callback = {
print("Button pressed", indexPath)
}
return cell
}When the button is pressed the callback is called. The index path is captured.
Edit
There is a caveat if cells can be added or removed. In this case pass the UITableViewCell
instance as parameter and get the index path from there
class ButtonCell: UITableViewCell {
var callback : ((UITableViewCell) -> Void)?
@IBAction func buttonPressed(_ sender : UIButton) {
callback?(self)
}
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "ButtonCellIdentifier", for: indexPath) as! ButtonCell
let item = dataSourceArray[indexPath.row]
// do something with item
cell.callback = { cell in
let actualIndexPath = tableView.indexPath(for: cell)!
print("Button pressed", actualIndexPath)
}
return cell
}
If even the section
can change, well, then protocol/delegate may be more efficient.
Swift - How to detect an action button in UItableViewCell is pressed from ViewController?
You could go for a good old fashioned delegate pattern. This has the advantage of not coupling your view controller from your cell. Don't forget to make your delegate weak
to avoid retain cycles.
The you can find the cell index path from the table view. (I'm assuming by cell number you mean index path)
protocol CellDelegate: class {
func didTap(_ cell: Cell)
}
class Cell: UITableViewCell {
weak var delegate: CellDelegate?
@IBAction func buttonPressed(_ sender: Any) {
delegate?.didTap(self)
}
}
class ViewController: UIViewController, CellDelegate {
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = ...
cell.delegate = self
return cell
}
func didTap(_ cell: Cell) {
let indexPath = self.tableView.indexPath(for: cell)
// do something with the index path
}
}
Detecting which UIButton was pressed in a UITableView
In Apple's Accessory sample the following method is used:
[button addTarget:self action:@selector(checkButtonTapped:) forControlEvents:UIControlEventTouchUpInside];
Then in touch handler touch coordinate retrieved and index path is calculated from that coordinate:
- (void)checkButtonTapped:(id)sender
{
CGPoint buttonPosition = [sender convertPoint:CGPointZero toView:self.tableView];
NSIndexPath *indexPath = [self.tableView indexPathForRowAtPoint:buttonPosition];
if (indexPath != nil)
{
...
}
}
Identify in which table view Cell button was pressed?
Use this line to get indexPath, Where you have to pass UIButton
on target selector
func buttonTapped(_ sender:AnyObject) {
let buttonPosition:CGPoint = sender.convert(CGPointZero, to:self.tableView)
let indexPath = self.tableView.indexPathForRow(at: buttonPosition)
}
How to get the indexPath.row when I click a button on a table view cell?
A common, generalized solution for this type of problem is to connect the @IBAction
of the button to a handler in the cell (not in the view controller), and then use a delegate-protocol pattern so the cell can tell the table when the button was tapped. The key is that when the cell does this, it will supply a reference to itself, which the view controller can then use to determine the appropriate indexPath (and thus the row).
For example:
Give your
UITableViewCell
subclass a protocol:protocol CustomCellDelegate: class {
func cell(_ cell: CustomCell, didTap button: UIButton)
}Hook up the
@IBAction
to the cell (not the view controller) and have that call the delegate method:class CustomCell: UITableViewCell {
weak var delegate: CustomCellDelegate?
@IBOutlet weak var customLabel: UILabel!
func configure(text: String, delegate: CustomCellDelegate) {
customLabel.text = text
self.delegate = delegate
}
@IBAction func didTapButton(_ button: UIButton) {
delegate?.cell(self, didTap: button)
}
}Obviously, when the cell is created, call the
configure
method, passing, amongst other things, a reference to itself as the delegate:extension ViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { ... }
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "CustomCell", for: indexPath) as! CustomCell
let text = ...
cell.configure(text: text, delegate: self)
return cell
}
}Finally, have the delegate method call
indexPath(for:)
to determine the index path for the cell in question:extension ViewController: CustomCellDelegate {
func cell(_ cell: CustomCell, didTap button: UIButton) {
guard let indexPath = tableView.indexPath(for: cell) else { return }
// use `indexPath.row` here
}
}
The other approach is to use closures, but again using the same general pattern of hooking the button @IBAction
to the cell, but have it call a closure instead of the delegate method:
Define custom cell with closure that will be called when the button is tapped:
class CustomCell: UITableViewCell {
typealias ButtonHandler = (CustomCell) -> Void
var buttonHandler: ButtonHandler?
@IBOutlet weak var customLabel: UILabel!
func configure(text: String, buttonHandler: @escaping ButtonHandler) {
customLabel.text = text
self.buttonHandler = buttonHandler
}
@IBAction func didTapButton(_ button: UIButton) {
buttonHandler?(self)
}
}When the table view data source creates the cell, supply a handler closure:
extension ViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { ... }
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "CustomCell", for: indexPath) as! CustomCell
let text = ...
cell.configure(text: text, buttonHandler: { [weak self] cell in // the `[weak self]` is only needed if this closure references `self` somewhere
guard let indexPath = tableView.indexPath(for: cell) else { return }
// use `indexPath` here
})
return cell
}
}
I personally prefer the delegate-protocol pattern, as it tends to scale more nicely, but both approaches work.
Note, in both examples, I studiously avoided saving the indexPath
in the cell, itself (or worse, “tag” values). By doing this, it protects you from getting misaligned if rows are later inserted and deleted from the table.
By the way, I used fairly generic method/closure names. In a real app, you might give them more meaningful names, e.g., didTapInfoButton
, didTapSaveButton
, etc.) that clarifies the functional intent.
Related Topics
How to Set Bold and Italic on Uilabel of Iphone/Ipad
How to Import Own Classes from Your Own Project into a Playground
Retrieving Carrier Name from iPhone Programmatically
Method 'Application:Openurl:Options:' Is Not Called
Swift - Stored Values Order Is Completely Changed in Dictionary
Using Nsuserdefaults on Arrays
Close iOS Keyboard by Touching Anywhere Using Swift
Ios7, Segue and Storyboards - How to Create Without a Button
Understanding Performseguewithidentifier
Is This Possible to Apply the Alternative Icon to the iOS Application
Ld: File Not Found: Linker Command Failed with Exit Code 1
How Add Separator to String at Every N Characters in Swift
How to Animate the Background Color of a Uilabel