I need to detect if the button has been clicked in the UITableViewController
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let LikesBtn = cell.viewWithTag(7) as! UIButton
}
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.
First step:
Make Subclass for your custom UITableViewCell, also register protocol.
Something like this:
protocol MyTableViewCellDelegate: class {
func onButtonPressed(_ sender: UIButton, indexPath: IndexPath)
}
class MyTableViewCell: UITableViewCell {
#IBOutlet var cellButton: UIButton!
var cellIndexPath: IndexPath!
weak var delegate: MyTableViewCellDelegate!
override func awakeFromNib() {
super.awakeFromNib()
cellButton.addTarget(self, action: #selector(self.onButton(_:)), for: .touchUpInside)
}
func onButton(_ sender: UIButton) {
delegate.onButtonPressed(sender, indexPath: cellIndexPath)
}
}
In your TableViewController, make sure it conform to your just created protocol "MyTableViewCellDelegate".
Look at the code below for better understanding.
class MyTableViewController: UITableViewController, MyTableViewCellDelegate {
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if let cell = tableView.dequeueReusableCell(withIdentifier: "cellId", for: indexPath) as? MyTableViewCell {
cell.cellIndexPath = indexPath
cell.delegate = self
return cell
} else {
print("Something wrong. Check your cell idetifier or cell subclass")
return UITableViewCell()
}
}
func onButtonPressed(_ sender: UIButton, indexPath: IndexPath) {
print("DID PRESSED BUTTON WITH TAG = \(sender.tag) AT INDEX PATH = \(indexPath)")
}
}
Here is what I use:
First initialize the button as an Outlet and its action on your TableViewCell
class MainViewCell: UITableViewCell {
#IBOutlet weak var testButton: UIButton!
#IBAction func testBClicked(_ sender: UIButton) {
let tag = sender.tag //with this you can get which button was clicked
}
}
Then in you Main Controller in the cellForRow function just initialize the tag of the button like this:
class MainController: UIViewController, UITableViewDelegate, UITableViewDataSource, {
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell") as! MainViewCell
cell.testButton.tag = indexPath.row
return cell
}
}
Related
I have a TableView and for it I created a cell in xib. This cell has a label and a button. I would like to display the label and hide the button under certain conditions for the cell I need. How can i do this?
extension SourcesViewController: UITableViewDelegate, UITableViewDataSource {
...
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 2
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: SourceCell.reuseId, for: indexPath) as! SourceCell
...
return cell
}
class SourceCell: UITableViewCell {
...
#IBOutlet var logInLabel: UILabel!
#IBOutlet var logOutButton: UIButton!
...
#IBAction func logInLogOutButton(_ sender: Any) {
//When I press this button I want to hide log out button and show label for current cell
}
}
My Custom Cell
import Foundation
import UIKit
class MyCustomCell : UITableViewCell {
#IBOutlet weak var myCustomLabel : UILabel!
#IBOutlet weak var myCustomButton : UIButton!
#IBAction func buttonClicked(_ sender : UIButton) {
sender.isHidden = true
}
}
TableView :
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "customCell", for: indexPath) as! MyCustomCell
cell.myCustomLabel.text = "Row \(indexPath.row)"
// Configure the cell...
return cell
}
Here is what the output looks like :
On my custom tableviewcell I have a button which the user can press which toggles between selected and unselected. This is just a simple UIButton, this button is contained with my customtableviewcell as an outlet.
import UIKit
class CustomCellAssessment: UITableViewCell {
#IBOutlet weak var name: UILabel!
#IBOutlet weak var dateTaken: UILabel!
#IBOutlet weak var id: UILabel!
#IBOutlet weak var score: UILabel!
#IBOutlet weak var button: UIButton!
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
#IBAction func selectButton(_ sender: UIButton) {
if sender.isSelected{
sender.isSelected = false
}else{
sender.isSelected = true
}
}
}
The strange thing is, when I press a button on say the first cell it then selects a button 8 cells down on another cell (out of the view) in the same tableview. Each cell has its own button but it is as if using dequeReusableCell is causing the system to behave this way. Why is it doing this and is it to do with the way that UIbuttons work on tableviewcells?
Thanks
Each cell has its own button but it is as if using dequeReusableCell is causing the system to behave this way.
Wrong. UITableViewCells are reusable, so if your tableView has 8 cells visible, when loading the 9th, the cell nr 1 will be reused.
Solution: You need to keep track of the state of your cells. When the method cellForRowAtIndexPath is called, you need to configure the cell from scratch.
You could have in your ViewController an small array containing the state of the cells:
var cellsState: [CellState]
and store there the selected state for each indexPath. Then in the cellForRowAtIndexPath, you configure the cell with the state.
cell.selected = self.cellsState[indexPath.row].selected
So, an overview of I would do is:
1 - On the cellForRowAtIndexPath, I would set
cell.button.tag = indexPath.row
cell.selected = cellsState[indexPath.row].selected
2 - Move the IBAction to your ViewController or TableViewController
3 - When the click method is called, update the selected state of the cell
self.cellsState[sender.tag].selected = true
Remember to always configure the whole cell at cellForRowAtIndexPath
Edit:
import UIKit
struct CellState {
var selected:Bool
init(){
selected = false
}
}
class MyCell: UITableViewCell {
#IBOutlet weak var button: UIButton!
override func awakeFromNib() {
self.button.setTitleColor(.red, for: .selected)
self.button.setTitleColor(.black, for: .normal)
}
}
class ViewController: UIViewController, UITableViewDataSource {
var cellsState:[CellState] = []
#IBOutlet weak var tableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
//Add state for 5 cells.
for _ in 0...5 {
self.cellsState.append(CellState())
}
}
#IBAction func didClick(_ sender: UIButton) {
self.cellsState[sender.tag].selected = true
self.tableView.reloadData()
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return cellsState.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell") as! MyCell
cell.button.isSelected = self.cellsState[indexPath.row].selected
cell.button.tag = indexPath.row
return cell
}
}
When you tap the button, you're setting the selected state of the button. When the cell gets reused, the button is still selected - you're not doing anything to reset or update that state.
If button selection is separate to table cell selection, then you'll need to keep track of the index path(s) of the selected buttons as part of your data model and then update the selected state of the button in tableView(_: cellForRowAt:).
A table view will only create as many cells as it needs to display a screen-and-a-bits worth of information. After that, cells are reused. If a cell scrolls off the top of the screen, the table view puts it in a reuse queue, then when it needs to display a new row at the bottom as you scroll, it pulls that out of the queue and gives it to you in tableView(_: cellForRowAt:).
The cell you get here will be exactly the same as the one you used 8 or so rows earlier, and it's up to you to configure it completely. You can do that in cellForRow, and you also have an opportunity in the cell itself by implementing prepareForReuse, which is called just before the cell is dequeued.
In your tableView(_: cellForRowAt:) take action for button click and add target
cell.button.addTarget(self, action: #selector(self.selectedButton), for: .touchUpInside)
At the target method use below lines to get indexPath
func selectedButton(sender: UIButton){
let hitPoint: CGPoint = sender.convert(CGPoint.zero, to: self.tableView)
let indexPath: NSIndexPath = self.tableView.indexPathForRow(at: hitPoint)! as NSIndexPath
}
Then do your stuff by using that indexPath.
Actually your method can not find in which indexPath button is clicked that's why not working your desired button.
One way to approach your problem is as follow
1. First create a protocol to know when button is clicked
protocol MyCellDelegate: class {
func cellButtonClicked(_ indexPath: IndexPath)
}
2. Now in your cell class, you could do something like following
class MyCell: UITableViewCell {
var indexPath: IndexPath?
weak var cellButtonDelegate: MyCellDelegate?
func configureCell(with value: String, atIndexPath indexPath: IndexPath, selected: [IndexPath]) {
self.indexPath = indexPath //set the indexPath
self.textLabel?.text = value
if selected.contains(indexPath) {
//this one is selected so do the stuff
//here we will chnage only the background color
backgroundColor = .red
self.textLabel?.textColor = .white
} else {
//unselected
backgroundColor = .white
self.textLabel?.textColor = .red
}
}
#IBAction func buttonClicked(_ sender: UIButton) {
guard let delegate = cellButtonDelegate, let indexPath = indexPath else { return }
delegate.cellButtonClicked(indexPath)
}
}
3. Now in your controller. I'm using UItableViewController here
class TheTableViewController: UITableViewController, MyCellDelegate {
let cellData = ["cell1","cell2","cell3","cell4","cell2","cell3","cell4","cell2","cell3","cell4","cell2","cell3","cell4","cell2","cell3","cell4","cell2","cell3","cell4"]
var selectedIndexpaths = [IndexPath]()
override func viewDidLoad() {
super.viewDidLoad()
tableView.register(MyCell.self, forCellReuseIdentifier: "MyCell")
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return cellData.count
}
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 55.0
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "MyCell", for: indexPath) as! MyCell
cell.cellButtonDelegate = self
cell.configureCell(with: cellData[indexPath.row], atIndexPath: indexPath, selected: selectedIndexpaths)
return cell
}
override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return 30.0
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let cell = tableView.cellForRow(at: indexPath) as! MyCell
cell.buttonClicked(UIButton())
}
func cellButtonClicked(_ indexPath: IndexPath) {
if selectedIndexpaths.contains(indexPath) {
//this means cell has already selected state
//now we will toggle the state here from selected to unselected by removing indexPath
if let index = selectedIndexpaths.index(of: indexPath) {
selectedIndexpaths.remove(at: index)
}
} else {
//this is new selection so add it
selectedIndexpaths.append(indexPath)
}
tableView.reloadData()
}
}
I have created a tableView with two different labels and one textfield. Depending on the indexPath in which is selected the labels will display different text according to the array. I have created a CocoTouch Class file and made it type TableViewCell.
TableViewCell.swift
import UIKit
class Driver_Navigation_TableViewCell: UITableViewCell {
#IBOutlet weak var orderTextField: UITextField!
#IBOutlet weak var adressLabel: UILabel!
#IBOutlet weak var nameLabel: UILabel!
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
}
ViewController.swift
class ViewController: UIViewController, MGLMapViewDelegate, UITableViewDataSource, UITableViewDelegate, UITextFieldDelegate {
var allCellsText = [String]()
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
array.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! Driver_Navigation_TableViewCell
cell.adressLabel.text = passengersAdress[indexPath.row]
cell.nameLabel.text = passengersName[indexPath.row]
cell.orderTextField.placeholder = "\(indexPath.row + 1)."
return(cell)
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath as IndexPath) as! Driver_Navigation_TableViewCell
cell.orderTextField.tag = indexPath.row
cell.orderTextField.delegate = self // theField is your IBOutlet UITextfield in your custom cell
return cell
}
func textFieldDidEndEditing(textField: UITextField) {
allCellsText.append(textField.text!)
print(allCellsText)
}
}
You cant have duplicate UITableViewDataSource and UITableViewDelegate methods it won't work like that.
Also in your code you have not set the delegate for the textField in both the cellForRowAtIndexPath() methods.
If you want to have two table view in a single controller try the following
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return array.count
}
func tableView(_ tableView: UITableView, cellForRowAtIndexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! Driver_Navigation_TableViewCell
if tableView == self.tableView1 {
cell.adressLabel.text = passengersAdress[indexPath.row]
cell.nameLabel.text = passengersName[indexPath.row]
cell.orderTextField.placeholder = "\(indexPath.row + 1)."
}
else {
cell.orderTextField.tag = indexPath.row
}
cell.orderTextField.delegate = self
return(cell)
}
Create outlets for the Table View
I have a UIView inside a TableViewCell,when users tapped in this UIView,it go to another view controller and pass a data along.
So inside TableViewCellClass I done the following :
I set up a protocol and detected the "Tap" gesture,when user table the UIView.This part is working fine:
protocol MyDelegate : class {
func runThisFunction(myString : String)
}
class MyTableViewCell: UITableViewCell {
weak var delegate : MyDelegate?
...other code here
//here detect the tap
let tap = UITapGestureRecognizer(target: self, action: #selector(hereTapped))
self.myUIElementInThisCell.addGestureRecognizer(tap)
}
#objc func hereTapped(sender: UITapGestureRecognizer? = nil){
self.delegate?.runThisFunction(myString: "I am a man")
}
So in my view controller which contain this TableView,I done the following :
I extend out the MyDelegate as subclass,and then attach the protocol function inside it as below
class MyViewController: UIViewController,MyDelagate{
func runThisFunction(myString : String) {
print("Tapped in view controller")
self.performSegue(withIdentifier: "MySegue",sender : self)
}
override func viewDidLoad() {
super.viewDidLoad()
let tableViewCell = MyTableViewCell()
tableViewCell.delegate = self
}
}
Result:
After done all the stuff above,when I tapped the UIView,it didnt perform the segue as stated in MyViewControllerClass,even the print() command also didnt execute.
So what I missing out? Please give me a solution.Thanks
The problem is that the delegate for MyTableViewCell instances is not defined.
When you do:
override func viewDidLoad() {
super.viewDidLoad()
let tableViewCell = MyTableViewCell()
tableViewCell.delegate = self
}
You are setting a delegate for an object that will be destroyed just when the method viewDidLoad() finishes.
Solution 1
In order to avoid this, you have to set the delegate inside the cellForRow method.
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "identifier", for: indexPath) as! MyTableViewCell
cell.delegate = self
// Other configurations
return cell
}
Solution 2
You can also use the UITableViewDelegate methods in order to capture the user interaction with the cells.
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let cell = tableView.cellForRow(at: indexPath)
self.performSegue(withIdentifier: "MySegue",sender : self)
}
This way you avoid all the MyDelegate protocol thing. This would be my preferred option.
The problem is that these 2 lines:
let tableViewCell = MyTableViewCell()
tableViewCell.delegate = self
are not related to the shown cells in the table , it's a cell created on the fly so
set delegate in cellForRow for the cell that you will actually waiting a delegate trigger from them
cell.delegate = self
like this
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = areaSettTable.dequeueReusableCell(withIdentifier:cellID) as! MyTableViewCell
cell.delegate = self
}
Problem is in your viewDidLoad:
// you create a new cell
let tableViewCell = MyTableViewCell()
// set its delegate
tableViewCell.delegate = self
// and then the cell is not used for anything else
Basically you are not setting the delegate for the cells that are being presented, but for another instance that you create in viewDidLoad.
You have to set a delegate in cellForRowAt to make sure the proper cells get the delegate set:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "yourIdentifier", for: indexPath) as! MyTableViewCell
cell.delegate = self
return cell
}
This will set the delegate for those cells, that are presented.
Alternatively, I would recommend using didSelectRowAt from UITableViewDelegate (if your MyViewController implements it):
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if indexPath.row == 0 { // only if it is the first cell (I assume that MyTableViewCell is first)
runThisFunction(myString: "I am a man")
}
}
Delegation in not very good here, indeed if you want to change object that didn't passed to the cell explictitly.
My advice is to use closure:
typealias CellTapHandler = (String)->()
class CustomCell: UITableViewCell {
var handler: CellTapHandler?
#objc func hereTapped(sender: UITapGestureRecognizer? = nil) {
handler?("String or whatever you want to get back from cell.")
}
//...
}
and set up it from view controller
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "yourIdentifier", for: indexPath)
(cell as? MyTableViewCell).handler = { [weak self] string in }
return cell
}
PS: As I said, usually you want to pass object related to cell further. It's difficult to do with delegation, as you had to pass to cell some additional token, to determine object by the cell, o pass object itself breaking Model-View separation paradigm. But it can be done easily with closures:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let object = self.myData[indexPath.row]
let cell = tableView.dequeueReusableCell(withIdentifier: "yourIdentifier", for: indexPath)
(cell as? MyTableViewCell).handler = { [weak self] string in
self.performSegue(withIdentifier: "MySegue",sender : object)
}
return cell
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if (let myObj = sender as? MyObjType, let subsequentVC = segue.destination as? NextViewController) {
subsequentVC.selectedMyObject = myObj
}
}
I am making a simple client invoice list using a tableView with custom labels in each row. at the top you click the plus sign. This takes you to a second page where you type in your clients name. you hit save and it should add it to the label in the first view controller. I cannot get this to work.
my tableview storyboard has the file called ViewController.swift - in it
import UIKit
var invoiceList = [""]
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
#IBOutlet var clientTableView: UITableView!
#IBOutlet var clientLabel: UILabel!
public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int
{
return invoiceList.count
}
public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
{
let cell = tableView.dequeueReusableCell(withIdentifier: "clientItem", for: indexPath) as! UITableViewCell
let ClientName = invoiceList[indexPath.row]
cell.textLabel?.text = [clientInput]
return cell
}
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == UITableViewCellEditingStyle.delete
{
invoiceList.remove(at: indexPath.row)
clientTableView.reloadData()
}
}
override func viewDidAppear(_ animated: Bool) {
clientTableView.reloadData()
}
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
My second storyboard where you type in the text field. File is called AddInvoiceItem.swift. in it -
import UIKit
class AddInvoiceViewController: UIViewController {
var clientTextField:String!
#IBOutlet var clientInput: UITextField!
#IBAction func addItem(_ sender: Any) {
if clientInput.text != ""
{
invoiceList.append(clientInput.text!)
clientInput.text = ""
_ = navigationController?.popViewController(animated: true)
}
}
#IBAction func cancelBtn(_ sender: Any) {
_ = navigationController?.popViewController(animated: true)
}
}
My error keeps happening when I am trying to use the code line
public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
{
let cell = tableView.dequeueReusableCell(withIdentifier: "clientItem", for: indexPath) as! UITableViewCell
let ClientName = invoiceList[indexPath.row]
cell.textLabel?.text = [clientInput]
return cell
}
The cell.textLabel?.text = [clientInput]
I know this is wrong, but cannot figure it out!
Make a new cell class
class CustomCell: UITableViewCell {
//Make your outlets here, connect the outlets from cell in your storyboard like ClientNameLabel
}
Please select the cell on your Storyboard, and enter the name of your class in this field. In your case it would be CustomCell
then in ViewController
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
{
let cell = tableView.dequeueReusableCell(withIdentifier: "clientItem", for: indexPath) as! CustomCell
let ClientName = invoiceList[indexPath.row]
cell.ClientNameLabel.text = ClientName
return cell
}