I have a custom UITableViewCell subclass and its associated xib. I have a UILabel and a UIButton in this cell and I have wired the touch up inside action of the button to the subclass.
What I need is when that button in the cell is tapped, to get the indexpath of the cell which has that button. And maybe send it back to the view controller via a delegate or something.
Since I'm inside a UITableViewCell subclass, I can't use a solution like this because I don't have a reference to the tableview from inside the cell subclass. Upon further investigation I found another solution and I implemented it in Swift like this.
import UIKit
class ContactCell: UITableViewCell {
#IBOutlet weak var nameLabel: UILabel!
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
selectionStyle = .None
}
#IBAction func callButtonPressed(sender: UIButton) {
let indexPath = (self.superview as UITableView).indexPathForCell(self)
println("indexPath?.row")
}
}
But when I tap on the button, it crashes with an error message saying Swift dynamic cast failed.
Any idea what's wrong with my code?
Or I'm open to any other suggestions which would allow me to achieve the desired result in any other way.
Thank you.
Sounds like you need a delegate:
Delegates in swift?
Then just pass the cell itself as a parameter to the delegate, and then you can easily do tableView.indexPathForCell(cellFromDelegateMethod)
Hey you can use "Tag" of the button also.Inside the cellForRowAt method of table delegate u can tag the button with Indexpath.row . here is the example what i m tried to say.
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// get ur cell nib .As it has a button
cell.startOrConntinuBtn.addTarget(self, action: #selector(sumbitOrContinue), for: .touchUpInside)
cell.startOrConntinuBtn.tag = indexPath.row }
and in the touch method "sumbitOrContinue" -
func sumbitOrContinue(sender: UIButton!) {
let tag = sender.tag
// do what you want to do like this example
let detail = self.detailList[tag]
let vc = self.storyboard?.instantiateViewController(withIdentifier: "mockExamInt") as! MockWindowVc
vc.detailId = detail.id
self.navigationController?.pushViewController(vc, animated: true)}
UIButton.Type really does not have member superview, but sender have
var cell: UITableViewCell = sender.superview.superview as UITableViewCell
Related
I have a MainCollectionView used for scrolling between items, inside of one of these cells I have another collectionView with cells. In that collection view, I have a button for each cell. My question is how do I pass action from my button to my MainCollectionView when it is tapped? I did create protocol for that button in the cell but I don't know how to let MainCollectionView know when my button is tapped. I can call action from my cell class but I think it is better to run it in Model which is my MainCollectionView. Below is my button protocol.
protocol ThanhCaHotTracksCellDelegate: class {
func handleSeeAllPressed()}
weak var delegate: ThanhCaHotTracksCellDelegate?
#objc func handleSeeAllButton(){
delegate?.handleSeeAllPressed()
}
LIke NSAdi said, you're on the right track, but the delegate pattern is a bit much overhead for just a single task like notifying about a button press.
I prefer using closures, because they're lightweight and helps to keep related code together.
Using Closures
This is what I'm always doing in UITableView. So this will work in UICollectionView too.
class MyTableViewCell: UITableViewCell {
var myButtonTapAction: ((MyTableViewCell) -> Void)?
#IBAction func myButtonTapped(_ sender: Any) {
myButtonTapAction?(self)
}
}
So when I dequeue my cell and cast it to MyTableViewCell I can set a custom action like this:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "myCellReuseIdentifier", for: indexPath) as! MyTableViewCell
cell.myButtonTapAction = { cell in
// Put your button action here
// With cell you have a strong reference to your cell, in case you need it
}
}
Using direct reference
When you're dequeueing your UICollectionView cell you can obtain a reference to your button by casting the cell to your cell's custom subclass.
Then just do the following
cell.button.addTarget(self, action: #selector(didTapButton(_:)), forControlEvents: .TouchUpInside)
And outside have a function:
#objc func didTapButton(_ sender: UIButton) {
// Handle button tap
}
Downside of this is that you have no direct access to your cell. You could use button.superview? but it's not a good idea since your view hierarchy could change...
You're on the right track.
Make sure MainCollectionView (or the class that contains) it implements ThanhCaHotTracksCellDelegate protocol.
Then assign the delegate as self.
Something like...
class ViewController: ThanhCaHotTracksCellDelegate {
override func viewDidLoad() {
super.viewDidLoad()
subCollectionView.delegate = self
}
}
I have an extension of UICollectionViewCell class. When something is pressed in this cell, I am trying to notify the Controller. I am not quite sure if the protocol delegate pattern is the way to go about it. I am not sure how to use it in this case. I have the following class outside my extension of UICollectionViewCell class.
protocol bundleThreadsDelegate: class {
func bundleThreadsDidSelect(_ viewController: UIViewController)
}
And I have the following property:
public weak var delegate: bundleThreadsDelegate? in my extension.
I am not quite sure where to go on from Here. Please help.
You said "when something is pressed in this cell", not when the cell itself is pressed, so I assume you may want multiple actionable items in your cell. If that's what you really mean then in your UICollectionViewCell, you could simply add a UIButton (no need for delegates as mentioned here because everything can happen within the same view controller—use delegates when communicating between different objects):
class MyCollectionViewCell: UICollectionViewCell {
let someButton = UIButton()
...
}
When you create the UICollectionView, to make it easiest, set the view controller it's in as the data source:
let myCollection = UICollectionView(frame: .zero, collectionViewLayout: MyCollectionViewFlowLayout())
myCollection.dataSource = self
...
Then in your data source, which would be in something like MyViewController, give the button a target:
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let path = indexPath.item
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "myCell", for: indexPath) as! MyCollectionViewCell
cell.someButton.addTarget(self, action: #selector(someButtonAction(_:)), for: .touchUpInside)
return cell
}
And make sure that the action method is also in MyViewController along with the data source.
#objc func someButtonAction(_ sender: UIButton) {
print("My collection view cell was tapped")
}
Now you can have multiple buttons within one collection view cell that do different things. You can also pass in arguments from the cell to the button action for further customization.
However, if you want action when the entire cell is pressed, use the delegate method already mentioned (or make the entire cell a UIButton which is not as elegant but that's open to interpretation).
Use this CollectionView Delegate method to notify the ViewController when your cell is selected by a user.
func collectionView(_ collectionView: UICollectionView,
didSelectItemAt indexPath: IndexPath)
Chris is correct, assuming you're only interested in the whole cell being selected. If you have a button or something within your cell and it's that press event you're interested in then yeah, you could use a delegate.
As an aside, protocols usually start with an uppercase letter, i.e. BundleThreadsDelegate rather than bundleThreadsDelegate, but that's up to you. Your general approach could be something like this (note this is pseudo code):
protocol YourProtocol {
func didPressYourButton()
}
class YourCell {
#IBOutlet weak var yourButton: UIButton!
public weak var yourButtonDelegate: YourProtocol?
func awakeFromNib() {
super.awakeFromNib()
yourButton.addTarget(self, action: #selector(didPressYourButton), for: .touchUpInside)
}
func didPressYourButton() {
yourButtonDelegate?.didPressYourButton()
}
}
And then in your view controller's cellForRowAt function:
let cell = ...
cell.yourButtonDelegate = self
Then conform to the protocol and implement the method in your view controller:
extension YourViewController: YourProtocol {
func didPressYourButton() {
doAllTheThings()
}
}
I have ViewController with TableView. In each cell of this TableView there are few buttons (so I can't use didSelectRow).
I need to get parent ViewController from action when buttons are pressed. So I add this function to my custom cell class:
#IBAction func editBtnPressed (_ sender: Any) {
}
I need to get to self in order to add some subviews to it.
How can I access root ViewController as self?
I guess you should do it by creating property of your controller in cell class and assign property value when after creating cell in cellForRowAtIndexPath method.
For example:
in cell class
weak var yourController : YourViewController?
in cellForRowAtIndexPath
cell.yourController = self
then you can access the YourViewController in editBtnPressed action.
But i suggest you to do by creating button action programmatically in your controller class. that's the good approach.
for example:
class YourCellClass: UITableViewCell {
#IBOulet var myBtn: UIbutton!
...
}
Yourcontroller class
in cellForRowAtIndexPath
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = self.tableView.dequeueReusableCell(withIdentifier: cellReuseIdentifier) as! YourCellClass
cell.myButton.addTarget(self, action: #selector(self. editBtnPressed), for: .touchUpInside)
and in editBtnPressed
func editBtnPressed (_ sender: Any) {
// you can access controller here by using self
}
This question already has answers here:
Correct way to setting a tag to all cells in TableView
(3 answers)
Closed 6 years ago.
I have a custom UITableViewCell in which I have connected my UIButton using Interface Builder
#IBOutlet var myButton: UIButton!
Under cell configuration of UITableViewController, I have the following code:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
var customCell = tableView.dequeueReusableCell(withIdentifier: self.MY_CELL_IDENTIFIER, for: indexPath) as! myCustomCell
// CONFIGURE OTHER CELL PARAMETERS
customCell.myButton.tag = indexPath.row
customCell.myButton.addTarget(self, action: #selector(myButtonPressed), for: UIControlEvents.touchUpInside)
return customCell
}
Finally, I have
private func myButtonPressed(sender: UIButton!) {
let row = sender.tag
print("Button Sender row: \(row)")
}
This code is not working, unless I change the function definition to below:
#objc private func myButtonPressed(sender: UIButton!) {
let row = sender.tag
print("Button Sender row: \(row)")
}
Is there a better way to implement UIButton on custom UITableViewCell in Swift 3
I am not a big fan using view tags. Instead of this, you could use the delegate pattern for your viewController to be notified when a button has been hit.
protocol CustomCellDelegate: class {
func customCell(_ cell: UITableViewCell, didPressButton: UIButton)
}
class CustomCell: UITableViewCell {
// Create a delegate instance
weak var delegate: CustomCellDelegate?
#IBAction func handleButtonPress(sender: UIButton) {
self.delegate?.customCell(self, didPressButton: sender)
}
}
class ViewController: UIViewController {
#IBOutlet weak var tableView: UITableView!
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
var customCell = tableView.dequeueReusableCell(withIdentifier: "identifier", for: indexPath) as! CustomCell
// CONFIGURE OTHER CELL PARAMETERS
//Assign the cell's delegate to yourself
customCell.delegate = self
return customCell
}
}
extension ViewController: CustomCellDelegate {
// You get notified by the cell instance and the button when it was pressed
func customCell(_ cell: UITableViewCell, didPressButton: UIButton) {
// Get the indexPath
let indexPath = tableView.indexPath(for: cell)
}
}
Yes, there is a smarter and better way to do this. The main problem of your method is that it only work if no insert, delete or move cells operation occurs. Because anyone of these operations can change de indexPath of the cells that were created for a different indexPath.
The system I use is this:
1.- Create a IBAction in your cell class MyCustomCell (With uppercase M. It is a class, so name it properly).
2.- Connect the button to that action.
3.- Declare a protocol MyCustomCellDelegate with, at least, a method
func myCustomCellButtonAction(_ cell:MyCustomCell)
and add a property to MyCustomCell
var delegate : MyCustomCellDelegate?
4.- Set the view controller as MyCustomCellDelegate
In the method of MyCustomCell connected to the button invoke the delegate method:
delegate?.myCustomCellButtonAction( self )
5.- In the view controller, in the method cellForRowAt:
customCell.delegate = self
6.- In the view controller, in the method myCustomCellButtonAction:
func myCustomCellButtonAction( _ cell: MyCustomCell ) {
let indexPath = self.tableView.indexPathForCell( cell )
// ...... continue .....
}
You can also use delegates to do the same.
Directly map the IBAction of button in your custom UITableViewCell Class.
Implement the delegate methods in viewcontroller and On button action call the delegate method from custom cell.
I'm trying to change the color of a button when pressed. Currently its inside a table view cell.
The way I'm doing it is adding as so:
#IBAction func upVote(sender: AnyObject) {
sender.setImage(UIImage(named: "bUpVote"), forState: .Normal)
}
and this is done inside the cell class (not the view controller class).
It works, but the change also applies to every third cell that follows it for the rest of the table.
Any work around? Thanks!
There are many way to solve this issue, one of the method is as follows
Add this to your customCell class,
#objc protocol MyTableViewCellDelegate {
func controller(controller: MyTableViewCell, button: UIButton, selectedButtonIndexPath : NSIndexPath)
}
class MyTableViewCell: UITableViewCell {
var delegate: AnyObject?
var indexPath : NSIndexPath?
#IBOutlet weak var button: UIButton!//outlet of button
button Action
#IBAction func buttonAction(sender: UIButton)//IF the sender type is AnyObject, you have to change it as UIButton
{
self.delegate?.controller(self, button: sender, selectedButtonIndexPath: indexPath!)
}
Add this to your ViewController class that has UITableView
class MyTableViewController: UITableViewController, MyTableViewCellDelegate { // I created a subClass of UITableViewController, your's may be different
var arraySelectedButtonIndex : NSMutableArray = []//global declaration
Since i created my custom cell using xib, in viewDidLoad()
tableView.registerNib(UINib(nibName: "MyTableViewCell", bundle: nil), forCellReuseIdentifier: "CustomCell")//Since, I use custom cell in xib
define delegate of custom cell by adding this
func controller(controller: MyTableViewCell, button: UIButton, selectedButtonIndexPath : NSIndexPath)
{
if(arraySelectedButtonIndex .containsObject(selectedButtonIndexPath)==false)
{
arraySelectedButtonIndex.addObject(selectedButtonIndexPath)
button.setImage(UIImage(named: "bUpVote") , forState: .Normal)
}
else
{
arraySelectedButtonIndex.removeObject(selectedButtonIndexPath)//If you need to set Deselect image
button.setImage(UIImage(named: "deselectImage") , forState: .Normal)//If you need to set Deselect image
}
}
In tableView dataSource (cellForRowAtIndexPath)
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("CustomCell", forIndexPath: indexPath) as! MyTableViewCell
cell.delegate = self
cell.indexPath = indexPath
if(arraySelectedButtonIndex .containsObject(indexPath))
{
cell.button.setImage(UIImage(named: "bUpVote"), forState: .Normal)
}
else
{
cell.button.setImage(UIImage(named: "deselectImage"), forState: .Normal)//If you need to set Deselect image
}
return cell
}
It's because cells are reused by the tableView. if you need to persist the state of subviews in the cell, you need to update your data source and reflect the changes in cellForRowAtIndexPath method.
This is not the way to do it. You store the state of the button in your model. Eg: say store the item's upvoted status in your model :
class Post
{
var title : String
var upvoted : Bool
}
How to get the index path ?
Move the IBAction method on your custom tableview subclass. Add a property called delegate to the cell and set it to your controller in cellForRowAtIndexPath: . Now in the action method inform the delegate.
I have described this in detail here : https://stackoverflow.com/a/32250043/1616513
Now when the user upvotes you update the model :
#IBAction func upVotedInCell(sender: UITableViewCell) {
var indexPath = self.tableView.indexPathForCell(sender)
self.items[indexPath].upvoted = true
self.tableView.reloadRowsAtIndexPaths([indexPath],UITableViewRowAnimation.None)
}