I create a custom cell that contains a button, I need to create segue from this button to other VC but first of all, I would like to push an object with that segue.
I already try to use cell.button.tag, but I did not succeed.
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "showMap" {
let mapVC = segue.destination as! MapViewController
//guard let indexPath = tableView.indexPathForSelectedRow else { return }
mapVC.place = places[] // <- "here I need index of button in cell"
}
}
Instead of using the segue, handle the navigation programatically through a closure in UITableViewCell.
class CustomCell: UITableViewCell {
var buttonTapHandler: (()->())?
#IBAction func onTapButton(_ sender: UIButton) {
self.buttonTapHandler?()
}
}
In the above code, I've create a buttonTapHandler - a closure, that will be called whenever the button inside the cell is tapped.
Now, in cellForRowAt method when you dequeue the cell, set the buttonTapHandler of CustomCell.
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "CustomCell", for: indexPath) as! CustomCell
cell.buttonTapHandler = {[weak self] in
if let mapVC = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "MapViewController") as? MapViewController {
mapVC.place = places[indexPath.row]
self?.navigationController?.pushViewController(mapVC, animated: true)
}
}
return cell
}
In the above code, buttonTapHandler when called will push a new instance of MapViewController along with the relevant place based on the indexPath.
if you don't want to execute your code in didSelectRowAt method, another good approach in my opinion is to create a delegate of your custom cell. See the code below
// This is my custom cell class
class MyCustomCell: UITableViewCell {
// The button inside your cell
#IBOutlet weak var actionButton: UIButton!
var delegate: MyCustomCellDelegate?
#IBAction func myDelegateAction(_ sender: UIButton) {
delegate?.myCustomAction(sender: sender, index: sender.tag)
}
// Here you can set the tag value of the button to know
// which button was tapped
func configure(index: IndexPath){
actionButton.tag = index.row
}
}
protocol MyCustomCellDelegate {
func myDelegateAction(sender: UIButton, index: Int)
}
Delegate the ViewController where you use your custom cell.
class MyViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "MyCellIdentifier", for: indexPath) as! MyCustomCell
cell.configure(index: indexPath)
cell.delegate = self
return cell
}
}
And at the end customize your method extending your custom cell delegate
extension MyViewController: MyCustomCellDelegate {
func myDelegateAction(sender: UIButton, index: Int) {
// Do your staff here
}
}
I hope I was helpful.
In the custom cell:
import UIKit
protocol CustomCellDelegate: class {
func btnPressed(of cell: CustomCell?)
}
class CustomCell: UITableViewCell {
weak var delegate: CustomCellDelegate?
#IBAction func btnTapped(_ sender: UIButton) {
delegate?.btnPressed(of: self)
}
}
And in the view controller:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell: CustomCell = tableView.dequeueReusableCell(for: indexPath)
cell.delegate = self
return cell
}
extension ViewController: CustomCellDelegate {
func btnPressed(of cell: CustomCell?) {
if let cell = cell, let indexPath = tableView.indexPath(for: cell) {
// Your stuff here
}
}
}
Related
I have view controller in which there are multiple section of tableview. In section 0 I have multiple row . Each row having button named as Add Comments when I click on button it pushes me to other view controller having text field when i wrote something and press done button then through delegate I passes textfield data and set it in button title. But problem is my button present in all row changes value. I want only selected row in section changes its button title. below is my code of first viewcontroller
class MyTabViewController: UIViewController {
var addCommentsValueStore: String = "Add Comments"
#IBOutlet weak var tabTableView : ContentWrappingTableView!
#IBAction func addCommentsAction(_ sender: UIButton) {
guard let nextVC = MyCommentsRouter.getMyCommentsViewScreen() else { return }
nextVC.passAddCommentsDelegate = self
self.navigationController?.pushViewController(nextVC, animated: true)
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if(indexPath.section == 0)
{
let indetifier = "MyTabTableViewCell"
let cell = tableView.dequeueReusableCell(withIdentifier: indetifier, for: indexPath) as! MyTabTableViewCell
cell.addCommentsButton.setTitle(addCommentsValueStore, for: UIControl.State.normal)
}
return cell
}
extension MyTabViewController: AddCommentsDelegate{
func passAddComments(instruction: String) {
addCommentsValueStore = instruction
print(addCommentsValueStore)
}
}
}
below is code of second view controller:
import UIKit
protocol AddCommentsDelegate{
func passAddComments(instruction: String)
}
class MyCommentsViewController: UIViewController {
#IBOutlet var addCommentsTextField: UITextField!
var passAddCommentsDelegate: AddCommentsDelegate?
override func viewDidLoad() {
super.viewDidLoad()
}
#IBAction func backActionClick(_ sender: UIButton) {
self.navigationController?.popViewController(animated: true)
}
#IBAction func DoneActionClick(_ sender: Any) {
let dataToBeSent = addCommentsTextField.text
self.passAddCommentsDelegate?.passAddComments(instruction: dataToBeSent!)
self.navigationController?.popViewController(animated: true)
}
}
by using tag on the cell button, you can fix your problem.
on your cellForRowAt delegate method, put this line:
cell.addCommentsButton.tag = indexPath.item
now you know exactly which button did select, then you can use this tag to specify which row in your tableView should change its title.
your implementation has some problems:
first of all, the addCommentsValueStore have to be an array of strings.
var addCommentsValueStore: [String] = []
then tell the delegate to show the right title:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
var cell: MyTabTableViewCell!
if (indexPath.section == 0) {
cell = tableView.dequeueReusableCell(withIdentifier: "MyTabTableViewCell", for: indexPath) as! MyTabTableViewCell
cell.tag = indexPath.item
cell.addCommentsButton.setTitle(addCommentsValueStore[indexPath.item], for: UIControl.State.normal)
}
return cell
}
the AddCommentsDelegate should return the index too:
protocol AddCommentsDelegate {
func passAddComments(instruction: String, atIndex: Int)
}
then every time you want to pass comment to another viewController, you should pass the index too.
by using this index, you will specify where you should change the row button title.
#UIBotton func DoneActionClick(_ sender: UIButton) {
let dataToBeSent = addCommentsTextField[sender.tag].text
self.passAddCommentsDelegate?.passAddComments(instruction: dataToBeSent!, atIndexPath: sender.tag)
self.navigationController?.popViewController(animated: true)
}
So I defined the ClickableCell protocol which is supposed to respond to tap gestures on an image. I want to redirect to ViewController2 when the user taps on the image, so my approach is to define a protocol, pass the info of tap to the CurrentViewController, and use the protocol in CurrentViewController to redirect to ViewController2. But I used print statements and it only printed "TAP TAP TAP" and "DONE" but not the portion from CurrentViewController. Can anyone help me figure out the problem? Thanks!
MessageCell.swift
protocol ClickableCell {
func onTapImage(indexPath: IndexPath)
}
class MessageCell: UITableViewCell {
#IBOutlet weak var leftImage: UIImageView!
var cellDelegate: ClickableCell?
var index: IndexPath?
override func awakeFromNib() {
super.awakeFromNib()
self.setupLabelTap()
}
#objc func imageTapped(tapGestureRecognizer: UITapGestureRecognizer){
print("TAP TAP TAP")
cellDelegate?.onTapImage(indexPath: index!)
print("DONE")
}
func setupLabelTap() {
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.imageTapped(tapGestureRecognizer:)))
self.leftImage.isUserInteractionEnabled = true
self.leftImage.addGestureRecognizer(tapGestureRecognizer)
}
}
CurrentViewController
extension CurrentViewController: ClickableCell {
func onTapImage(indexPath: IndexPath) {
print("Doesn't Print This")
let popOverVC = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(identifier: "nextView") as! UIViewController
self.navigationController?.pushViewController(popOverVC, animated: true)
}
}
You need to assign that cellDelegate when you create cell
extension CurrentViewController: UITableViewDataSource {
// other your code here
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "your_message_cell_id",
for: indexPath) as! MessageCell
/// other setup code here
cell.cellDelegate = self // << here !!
return cell
}
}
You need to set the delegate. The core purpose of the delegate pattern is to allow an object to communicate back to its owner in a decoupled way.
So in cellForRowAt method:-
cell.delegate = self
I have one table view and inside that i placed one main view. And inside that main view i placed one button.And when ever use click on my cell button. I need to get the cell title label.This is what i need. But i tried following below code. Not sure what i am missing out. It not at all calling my cell.add target line.
Code in cell for row at index:
cell.cellBtn.tag = indexPath.row
cell.cellBtn.addTarget(self, action:#selector(self.buttonPressed(_:)), for:.touchUpInside)
#objc func buttonPressed(_ sender: AnyObject) {
print("cell tap")
let button = sender as? UIButton
let cell = button?.superview?.superview as? UITableViewCell
let indexPath = tableView.indexPath(for: cell!)
let currentCell = tableView.cellForRow(at: indexPath!)! as! KMTrainingTableViewCell
print(indexPath?.row)
print(currentCell.cellTitleLabel.text)
}
I even added a breakpoint, still it not at calling my cell.addTarget line
Tried with closure too. In cell for row at index:
cell.tapCallback = {
print(indexPath.row)
}
In my table view cell:
var tapCallback: (() -> Void)?
#IBAction func CellBtndidTap(_ sender: Any) {
print("Right button is tapped")
tapCallback?()
}
Here that print statement is getting print in console.
import UIKit
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
var list = [String]()
#IBOutlet weak var tableView: UITableView!
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return list.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! MyTableViewCell
cell.saveButton.tag = indexPath.row
//cell.saveButton.accessibilityIdentifier = "some unique identifier"
cell.tapCallback = { tag in
print(tag)
}
return cell
}
}
class MyTableViewCell: UITableViewCell {
// MARK: - IBOutlets
#IBOutlet weak var saveButton: UIButton!
// MARK: - IBActions
#IBAction func saveTapped(_ sender: UIButton) {
tapCallback?(sender.tag)
}
// MARK: - Actions
var tapCallback: ((Int) -> Void)?
}
Actually this is not a good programming practice to add the button (which contains in table view cell) target action in view controller. We should follow the protocol oriented approach for it. Please try to under stand the concept.
/*This is my cell Delegate*/
protocol InfoCellDelegate {
func showItem(item:String)
}
/*This is my cell class*/
class InfoCell: UITableViewCell {
//make weak reference to avoid the Retain Cycle
fileprivate weak var delegate: InfoCellDelegate?
//Outlet for views
#IBOutlet var showButton: UIButton?
override func awakeFromNib() {
super.awakeFromNib()
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
}
//This is the public binding function which will bind the data & delegate to cell
func bind(with: DataModel?, delegate: InfoCellDelegate?, indexPath: IndexPath) {
//Now the bind the cell with data here
//.....
//Assign the delegate
self.delegate = delegate
}
//Button action
#IBAction func rowSelected(sender: UIButton) {
self.delegate?.showItem(item: "This is coming from cell")
}
}
/*Now in your ViewController you need to just confirm the InfoCellDelegate & call the bind function*/
class ListViewController: UIViewController {
//Views initialisation & other initial process
}
//Table view Delegate & Data source
extension ListViewController: UITableViewDataSource, UITableViewDelegate {
/**
Configure the table views
*/
func configureTable() {
//for item table
self.listTable.register(UINib.init(nibName: "\(InfoCell.classForCoder())", bundle: nil), forCellReuseIdentifier: "\(InfoCell.classForCoder())")
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 5
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "InfoCell") as! InfoCell
cell.bind(with: DataModel, delegate: self, indexPath: indexPath)
return cell
}
}
extension ListViewController: InfoCellDelegate {
func showItem(item) {
print(item)
}
}
I have a tableview with one textfield in each cell. I added a target like this:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "customLevelCell") as! LevelTableViewCell
cell.cellTextField.addTarget(self, action: #selector(ViewController.TextfieldEditAction), for: .editingDidEnd)
return cell
}
But found out that I'm not able to use the indexpath.row / sender.tag to get the specific textfield text
#objc func TextfieldEditAction(sender: UIButton) {
}
So my question is how can I get the text after the user has edited one of the textfields.
Also how can i get the indexpath.row or sender.tag which will be used to collect the text they added to that specific textfield.
The easiest way to handle this is probably to use a delegate protocol…
In your cell
protocol LevelTableViewCellDelegate: class {
func levelTableViewCell(_ levelTableViewCell: LevelTableViewCell, didEndEditingWithText: String?)
}
class LevelTableViewCell: UITableViewCell {
#IBOutlet private weak var cellTextField: UITextField!
var delegate: LevelTableViewCellDelegate?
override func awakeFromNib() {
cellTextField.addTarget(self, action: #selector(didEndEditing(_:)), for: .editingDidEnd)
}
#objc func didEndEditing(_ sender: UITextField) {
delegate?.levelTableViewCell(self, didEndEditingWithText: sender.text)
}
}
In your view controller
class TableViewController: UITableViewController {
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "LevelTableViewCell") as! LevelTableViewCell
cell.delegate = self
return cell
}
}
extension TableViewController: LevelTableViewCellDelegate {
func levelTableViewCell(_ levelTableViewCell: LevelTableViewCell, didEndEditingWithText: String?) {
let indexPath = tableView.indexPath(for: levelTableViewCell)
// Now you have the cell, indexPath AND the string
}
Also, note that the view outlet is be private. You'll find that you write cleaner code if you follow this rule
Following is the extension of UIView that can be used to get the cell or indexPath of the cell enclosing textField
extension UIView {
var tableViewCell : UITableViewCell? {
var subviewClass = self
while !(subviewClass is UITableViewCell){
guard let view = subviewClass.superview else { return nil }
subviewClass = view
}
return subviewClass as? UITableViewCell
}
func tableViewIndexPath(_ tableView: UITableView) -> IndexPath? {
if let cell = self.tableViewCell {
return tableView.indexPath(for: cell)
}
return nil
}
}
Example :-
#objc func TextfieldEditAction(sender: UITextField) {
//replace tableView with the name of your tableView
guard let indexPath = sender.tableViewIndexPath(tableView) else {return}
}
I have a tableView in a SubMenuViewController, when a user taps (using didSelectRowAt) on a cell and segues, I need to pass that cell to the next UserInputViewController,
Here is my code:
class SubMenuViewController: UIViewController {
//MARK: - Properties and outlets
var node: Node?
#IBOutlet weak var tableView: UITableView!
//MARK: - View controller methods
override func viewDidLoad() {
super.viewDidLoad()
self.navigationController?.isNavigationBarHidden = false
self.navigationItem.title = node?.value.rawValue
let nib = UINib(nibName: "SubMenuTableViewCell", bundle: nil)
tableView.register(nib, forCellReuseIdentifier: "SubMenuCell")
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "userInput" {
let vc = segue.destination as! UserInputViewController
let indexPath = sender as! IndexPath
vc.node = node?.childenNode[indexPath.row]
}
}
}
//MARK: UITableViewDataSource methods
extension SubMenuViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return node!.childCount
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "SubMenuCell", for: indexPath) as! SubMenuTableViewCell
let desciptionModule = node?.childenNode[indexPath.row].value
let description = Modules.description(module: desciptionModule!)
cell.title.text = description.main
cell.subtitle.text = description.sub
return cell
}
}
//MARK: - UITableViewDelegate methods
extension SubMenuViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 68
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
guard let selectedNode = node?.childenNode[indexPath.row] else {
return
}
if selectedNode.isLeaveNode() {
performSegue(withIdentifier: "userInput", sender: indexPath)
} else {
let subMenuViewController = self.storyboard!.instantiateViewController(withIdentifier: "subMenu") as! SubMenuViewController
subMenuViewController.node = selectedNode
//let subMenuViewController = SubMenuViewController(node: selectedNode)
self.navigationController?.pushViewController(subMenuViewController, animated: true)
}
}
}
Right now, in my performSegue, I passed in my indexPath into the sender, and I should expect to get it back in prepareForSegue, but I can't. Any suggestions guys?
Thanks
In my opinion it isn't very good practice to pass the index path (or any other value that counts a s "data") as the sender argument; as its name suggests, it is intended for passing the object that sent the message (i.e., called the action method), in this case self (you could "relay" the original sender if your action method calls another action method instead, but that's off-topic here).
As #sCha kindly pointed out in the comments, the Apple documentation on this method in particular, though, seems to leave room for doubt nevertheless. The parameter name sender clearly comes from the homonimous argument in all control actions that follow Cocoa's target/action pattern.
My suggestion:
The best you can do I think is to store the index path in a property of your view controller:
var selectedIndexPath: IndexPath?
...set it on tableView(_:didSelectRowAt:):
if selectedNode.isLeaveNode() {
self.selectedIndexPath = indexPath
performSegue(withIdentifier: "userInput", sender: indexPath)
} else {
self.selectedIndexPath = nil
// ...
...and retrieve it (while resetting the property) in the prepareForSegue(_:sender:) implementation of the target view controller:
if let vc = segue.source as? SubmenuViewController {
if let indexPath = vc.selectedIndexPath {
vc.selectedIndexPath = nil // (reset it, just to be safe)
// Use indexPath...
}
}