I am implementing a Question & Answer program using Swift with Firebase. I want users to be able to like the answers of questions. My database structure for handling likes is:
answerLikes
answerID
userID : true
answers
...
posts
...
users
...
I tried to implement my program according to this data structure. You can see the code in my TableViewController:
#IBAction func likeButtonClicked(_ sender: UIButton) {
if let indexPath = self.tableView.indexPathForSelectedRow {
ref = Database.database().reference()
print(indexPath.row)
ref.child("answerLikes").child(answers[indexPath.row].id).observeSingleEvent(of: .value, with: {
(snapshot) in
let value = snapshot.value as? NSDictionary
if value?[Auth.auth().currentUser?.uid] == nil {
sender.setImage(UIImage(named: "filledHeart.png"), for: .normal)
self.ref.child("answerLikes").child(self.answers[indexPath.row].id).updateChildValues([(Auth.auth().currentUser?.uid)! : true])
} else {
sender.setImage(UIImage(named: "emptyHeart.png"), for: .normal)
self.ref.child("answerLikes").child(self.answers[indexPath.row].id).removeValue()
}
})
}
}
My problem is that in this function definition, I cannot know that "the tapped like button is in which cell?". We handle this issue in the table view function by using indexPath. So I tried to use it in this code too, however, my code works only if the user clicks the cell and then clicks the like button.
Can anyone help me with this please? I am really having serious problem with this "Like Post" feature. Thank you.
First way
If you are using a custom cell you can use a protocol:
protocol CustomCellDelegate: class {
func likeButtonClicked(cell: YourCell)
}
class YourCell: UITableViewCell {
weak var delegate: CustomCellDelegate?
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}
#IBAction func likeButtonTapped(sender: AnyObject){
delegate?.likeButtonClicked(self)
}
}
Then add the delegate to your ViewController and in cellForRowAtIndexPath set it for your cell:
cell.delegate = self
And finally you can use it in this way:
func likeButtonClicked(cell: YourCell) {
if let indexPath = self.tableView.indexPath(for: cell) {
//....
}
}
Second way
You can get the index with your button position:
#IBAction func likeButtonClicked(_ sender: UIButton) {
var buttonPosition = sender.convertPoint(.zero, to: self.tableView)
if let indexPath = self.tableView.indexPathForRow(at: buttonPosition) {
//.....
}
}
Third way
In cellForRowAtIndexPath you can use your button tag:
likeButton.tag = indexPath.row
And then:
#IBAction func likeButtonClicked(_ sender: UIButton) {
let cellRow = sender.tag
//...
}
In my project thus far, I have successfully implemented a tableview whose cells populate postings from FIR database.
I can't figure out what code to add so that when the message user button is pushed, the poster's userID specific to that cell is identified and extracted. If I could get that far, I can extract the rest of the other user's info so that I can set up a chat that includes the receiver of the message's userID, userFirstName, and profile pic.
I would guess that tagging the button is a first step, but I'm not sure how that gets me the userID specific to that posting.
I don't even have code to show because I'm clueless in how to do this....
Put the button in cell. Also make a custom class of cell and put the userId or user object in that cell. When you tap the button you can get the userId etc from that cell.
Or if you want to get that event from cell to viewcontroller you can pass delegate to view viewcontroller. Some thing like this
//cellforrowatindexpath
cell.delegate = self
cell.indexPath = indexPath
// Your cell class
protocol CellDelegate: class {
func didTapCell(index: IndexPath)
}
#IBAction func buttonAction(_ sender: AnyObject) {
self.delegate?.didTapCell(index: indexPath)
}
//tag the cell button
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("HealerProfileCell", forIndexPath: indexPath) as! yourTVCell
cell.detailsButton.tag = indexPath.row
cell.detailsButton.addTarget(self, action: #selector(HealersTableViewController.performHealerDetailsInfoSegue(_:)), forControlEvents: .TouchUpInside)
return cell
}
//make a function
func performHealerDetailsInfoSegue(sender: AnyObject?) {
performSegueWithIdentifier("your segue identifier", sender: sender)
}
//use prepare for segue method
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
let object = yourArray[(sender!.tag)]
if segue.identifier == "your segue identifier" {
let dvc = segue.destinationViewController as! YourVC
dvc.object = object
}
}
Add a target to your button in the TableView cellForItemAtIndexPath Method.
cell.yourButton.addTarget(self, action: #selector(CellButtonClicked), for: .touchUpInside)
Then add this function that is called when the button is pushed...
func CellButtonClicked(sender: UIButton) {
print("Clicked")
guard let cellInAction = sender.superview as? YourCell else { return }
guard let din = yourTableView?.indexPath(for: cellInAction) else { return }
// retrieve the specific values in the cell from your array
let specificCellData = yourArray[(indexPath as NSIndexPath).row]
}
I am developing app which users will choose one of the two pictures in one cell. My prototype cell looks like :
How can I detect when the user presses the vote button which cell is selected ?
My tableView Code :
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cellIdentifier = "NewTableViewCell"
let cell = tableView.dequeueReusableCellWithIdentifier(cellIdentifier, forIndexPath: indexPath) as! NewTableViewCell
//For left image
if let url:NSURL? = NSURL(string: self.polls[indexPath.row].poll_photo1 ){
cell.leftImage.sd_setImageWithURL(url)
}
//For right image
if let url:NSURL? = NSURL(string: self.polls[indexPath.row].poll_photo2 ){
cell.rightImage.sd_setImageWithURL(url)
}
//For user picture
if let url:NSURL? = NSURL(string: self.polls[indexPath.row].users[0].user_photo ){
cell.userPicture.sd_setImageWithURL(url)
}
// gets username and text
cell.userName.text=self.polls[indexPath.row].users[0].user_name
cell.description.text = self.polls[indexPath.row].poll_textfield
return cell
}
My web API:
I am assuming that your custom table view cell NewTableViewCell is having an outlet for your vote button.
Just tag your voteButton with indexPath.row and fetch the tag in its target function as shown below. You will get to know which cell's voteButton was tapped when you press your Vote Button
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("NewTableViewCell") as! NewTableViewCell
//Tagging with indexPath.row
cell.voteLeftButton.tag = indexPath.row
cell.voteRightButton.tag = indexPath.row
//This is the latest Swift 2.2 syntax for selector. If you are using the older version of Swift, Kindly check the selector syntax and make changes accordingly
cell.voteLeftButton.addTarget(self, action: #selector(voteLeftButtonPressed), forControlEvents: .TouchUpInside)
cell.voteRightButton.addTarget(self, action: #selector(voteRightButtonPressed), forControlEvents: .TouchUpInside)
return cell
}
func voteLeftButtonPressed(sender:UIButton){
print("Left Button Table cell clicked is \(sender.tag)")
}
func voteRightButtonPressed(sender:UIButton){
print("Right Button Table cell clicked is \(sender.tag)")
}
Add target/action to your button in cell configure method like this:
let tap : UITapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(YourController.tapAction(_:)))
Then implement tapAction method
func tapAction(sender : UITapGestureRecognizer)
{
if sender.state != UIGestureRecognizerState.Ended
{
return
}
let btn = sender.view as! UIButton
let pointTo : CGPoint = CGRectOffset(btn.bounds, btn.frame.size.width/2, btn.frame.size.height/2).origin;
let buttonPosition : CGPoint = btn.convertPoint(pointTo, toView: self.tableView)
let indexPath = self.tableView.indexPathForRowAtPoint(buttonPosition)
...
}
I currently have a sidebar menu in an iOS 9.2 app running on Xcode 7.2 written in Swift 2 that allows the user what data to load to populate the view. I'm using SWRevealViewController to create that sidebar. I have a container view controller which has the front page and the sidebar page listing all the options the user has. Each time the user selects a cell from the sidebar table, it performs a segue that allows the front page to be refreshed. What I want to do is to allow the user to delete a cell from the table with a long press. I'd like to show an AlertViewController to confirm the user's decision, and if "Yes" is selected, I want to delete the cell, and select the very first item in the table. I've tried following the instructions from Long press on UITableView
but I keep getting the error "unrecognized selector sent to instance"
Here's the code that I'm using for setting up the gestureRecognizer in the cellForRowAtIndexPath function:
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("cell", forIndexPath: indexPath) as UITableViewCell;
cell.textLabel!.text = tableArray[indexPath.row];
let holdToDelete = UILongPressGestureRecognizer(target: cell, action: "longPressDelete");
holdToDelete.minimumPressDuration = 1.00;
cell.addGestureRecognizer(holdToDelete);
return cell;
}
And here's the longPressDelete function:
func longPressDelete(sender: UILongPressGestureRecognizer) {
let alert: UIAlertController = UIAlertController(title: "Please Confirm", message: "Are you sure you want to delete this car from your database?", preferredStyle: .Alert);
alert.addAction(UIAlertAction(title: "Yes", style: .Destructive, handler: { (UIAlertAction) -> Void in
if let tv = self.tableView {
let point: CGPoint = sender.locationInView(self.tableView);
let indexPath: NSIndexPath = self.tableView.indexPathForRowAtPoint(point)!;
tv.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Fade);
NSUserDefaults.standardUserDefaults().removeObjectForKey("fillUp" + tableArray[indexPath.row]);
NSUserDefaults.standardUserDefaults().removeObjectForKey("services" + tableArray[indexPath.row]);
tableArray.removeAtIndex(indexPath.row);
NSUserDefaults.standardUserDefaults().setObject(tableArray, forKey: "cars");
self.deleted = true;
self.performSegueWithIdentifier("tableToDashboard", sender: self);
}
}));
alert.addAction(UIAlertAction(title: "No", style: .Default, handler: nil));
self.presentViewController(alert, animated: true, completion: nil);
}
Here's the prepareForSegue function:
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if (deleted) {
let indexPath: NSIndexPath = NSIndexPath(forRow: 0, inSection: 0);
fillUpKey = "fillUp" + tableArray[indexPath.row];
servicesKey = "services" + tableArray[indexPath.row];
localFillUpArray = [fillUp]();
} else {
let indexPath: NSIndexPath = self.tableView.indexPathForSelectedRow!;
fillUpKey = "fillUp" + tableArray[indexPath.row];
servicesKey = "services" + tableArray[indexPath.row];
localFillUpArray = [fillUp]();
}
}
What I'd like to happen is that the user deletes the item in the cell, and the app then performs a segue to the front screen after loading the information from another source. Thanks for taking the time to read this and possibly provide an answer. I hope I haven't made a rookie mistake somewhere.
Incorrect Selector
let holdToDelete = UILongPressGestureRecognizer(target: self,
action: "longPressDelete:");
: after longPressDelete indicates that the method func longPressDelete(sender: UILongPressGestureRecognizer) actually accepts parameters.
self for target, assuming that the selector belongs to the same class that registered it.
The current selector "longPressDelete" would match a method signature without parameters:
func longPressDelete() { }
Very simple example if you want to select cell in uitableview
let longGesture = UILongPressGestureRecognizer(target: self, action: #selector(ViewController.longTap))
cell.addGestureRecognizer(longGesture)
// longTap
func longTap(gestureReconizer: UILongPressGestureRecognizer) {
print("Long tap")
let longPress = gestureReconizer as UILongPressGestureRecognizer
_ = longPress.state
let locationInView = longPress.location(in: tableview)
let indexPath = tableview.indexPathForRow(at: locationInView)
// whatever you want with indexPath use it //
}
I have a tableview with a variable number of cells representing students that correspond to their particular instructor. They are custom cells with a button that triggers a segue to a new VC, bringing up detailed information on the student whose cell it was. My question is:
What is the best practice in swift for identifying which button was pressed?
Once i know the index path, I can identify which student's information needs to be passed to the next VC. There is a great answer for objective C in the post below, but I'm not sure how to translate to Swift. Any help would be much appreciated.
Detecting which UIButton was pressed in a UITableView
If your code allows, I'd recommend you set the UIButton tag equal to the indexPath.row, so when its action is triggered, you can pull the tag and thus row out of the button data during the triggered method. For example, in cellForRowAtIndexPath you can set the tag:
button.tag = indexPath.row
button.addTarget(self, action: "buttonClicked:", forControlEvents: UIControlEvents.TouchUpInside)
then in buttonClicked:, you can fetch the tag and thus the row:
func buttonClicked(sender:UIButton) {
let buttonRow = sender.tag
}
Otherwise, if that isn't conducive to your code for some reason, the Swift translation of this Objective-C answer you linked to:
- (void)checkButtonTapped:(id)sender
{
CGPoint buttonPosition = [sender convertPoint:CGPointZero toView:self.tableView];
NSIndexPath *indexPath = [self.tableView indexPathForRowAtPoint:buttonPosition];
if (indexPath != nil)
{
...
}
}
is:
func checkButtonTapped(sender:AnyObject) {
let buttonPosition = sender.convert(CGPoint.zero, to: self.tableView)
let indexPath = self.tableView.indexPathForRow(at: buttonPosition)
if indexPath != nil {
...
}
}
Swift 3.0 Solution
cell.btnRequest.tag = indexPath.row
cell.btnRequest.addTarget(self,action:#selector(buttonClicked(sender:)), for: .touchUpInside)
func buttonClicked(sender:UIButton) {
let buttonRow = sender.tag
}
Updated for Swift 3
If the only thing you want to do is trigger a segue on a touch, it would be against best practice to do so via a UIButton. You can simply use UIKit's built in handler for selecting a cell, i.e. func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath). You could implement it doing something like the following:
Create a custom UITableViewCell
class StudentCell: UITableViewCell {
// Declare properties you need for a student in a custom cell.
var student: SuperSpecialStudentObject!
// Other code here...
}
When you load your UITableView, pass the data into the cell from you data model:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "StudentCell", for: indexPath) as! StudentCell
cell.student = superSpecialDataSource[indexPath.row]
return cell
}
Then use didSelectRow atIndexPath to detect when a cell has been selected, access the cell and it's data, and pass the value in as a parameter to performSegue.
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let cell = tableView.cellForRow(at: indexPath) as! StudentCell
if let dataToSend = cell.student {
performSegue(withIdentifier: "DestinationView", sender: dataToSend)
}
}
And finally in prepareForSegue:
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "DestinationView" {
let destination = segue.destination as! DestinationViewController
if let dataToSend = sender as? SuperSpecialStudentObject {
destination.student = dataToSend
}
}
}
Alternatively if you want them to only select a part of the cell instead of when they touch anywhere inside the cell, you could add an accessory item onto your cell such as the detail accessory item (looks like the circle with an "i" inside of it) and use override func tableView(_ tableView: UITableView, accessoryButtonTappedForRowWith indexPath: IndexPath) instead.
Another possible solution would be using dispatch_block_t. If you do it with Storyboard you first have to create a member variable in your custom UITableViewCell class.
var tapBlock: dispatch_block_t?
Then you have to create an IBAction and call the tapBlock.
#IBAction func didTouchButton(sender: AnyObject) {
if let tapBlock = self.tapBlock {
tapBlock()
}
}
In your view controller with the UITableView you can simply react to the button events like this
let cell = tableView.dequeueReusableCellWithIdentifier("YourCellIdentifier", forIndexPath: indexPath) as! YourCustomTableViewCell
cell.tapBlock = {
println("Button tapped")
}
However you have to be aware when accessing self inside the block, to not create a retain cycle. Be sure to access it as [weak self].
Swift 3
# cellForRowAt indexPath
cell.Btn.addTarget(self, action: #selector(self.BtnAction(_:)), for: .touchUpInside)
Then
func BtnAction(_ sender: Any)
{
let btn = sender as? UIButton
}
It's never a good idea to use tags to identify cells and indexPaths, eventually you'll end up with a wrong indexPath and consequently the wrong cell and information.
I suggest you try the code bellow (Working with UICollectionView, didn't tested it with a TableView, but it probably will work just fine):
SWIFT 4
#objc func buttonClicked(_ sender: UIButton) {
if let tableView = tableViewNameObj {
let point = tableView.convert(sender.center, from: sender.superview!)
if let wantedIndexPath = tableView.indexPathForItem(at: point) {
let cell = tableView.cellForItem(at: wantedIndexPath) as! SpecificTableViewCell
}
}
}
Detecting the Section and row for UiTableView indexPath on click Button click
//MARK:- Buttom Action Method
#objc func checkUncheckList(_sender:UIButton)
{
if self.arrayRequestList != nil
{
let strSection = sender.title(for: .disabled)
let dict = self.arrayRequestList![Int(strSection!)!]["record"][sender.tag]
print("dict:\(dict)")
self.requestAcceptORReject(dict: dict, strAcceptorReject: "1")
}
}
Here is UITableView Cell Method to add the targate
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "OtherPropertySelectiingCell", for: indexPath as IndexPath) as! OtherPropertySelectiingCell
cell.btnAccept.tag = indexPath.row
cell.btnAccept.setTitle("\(indexPath.section)", for: .disabled)
cell.btnAccept.addTarget(self, action: #selector(checkUncheckList(_sender:)), for: .touchUpInside)
return cell
}
Swift 5. In cellForRowAtIndexPath you set the tag:
cell.shareButton.tag = indexPath.row
cell.shareButton.addTarget(self, action: #selector(shareBtnPressed(_:)), for: .touchUpInside)
Then in shareBtnPressed you fetch the tag
#IBAction func shareBtnPressed(_ sender: UIButton) {
let buttonRow = sender.tag
print("Video Shared in row \(buttonRow)")
}
As a follow up to #Lyndsey and #longbow's comments, I noticed that when I had the segue in storyboard going from the button to the destinationVC, the prepareForSegue was being called before the buttonClicked function could update the urlPath variable. To resolve this, I set the segue directly from the first VC to the destinationVC, and had the segue performed programmatically after the code in buttonClicked was executed. Maybe not ideal, but seems to be working.
func buttonClicked(sender:UIButton) {
let studentDic = tableData[sender.tag] as NSDictionary
let studentIDforTherapyInt = studentDic["ID"] as Int
studentIDforTherapy = String(studentIDforTherapyInt)
urlPath = "BaseURL..."+studentIDforTherapy
self.performSegueWithIdentifier("selectTherapySegue", sender: sender)
}
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject!) {
if (segue.identifier == "selectTherapySegue") {
let svc = segue.destinationViewController as SelectTherapyViewController;
svc.urlPath = urlPath
}
Updated for Swift 5:
Place the following code within your ViewController class
#IBAction func buttonClicked(_ sender: UIButton) {
if let tableView = tableView {
let point = tableView.convert(sender.center, from: sender.superview!)
//can call wantedIndexPath.row here
}
}
}
I am doing it via prepareforSegue
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
let indexPath = self.tableView.indexPathForSelectedRow()
let item = tableViewCollection[indexPath!.row].id
let controller = segue.destinationViewController as? DetailVC
controller?.thisItem = item
}
and on the next controller i will just reload the full item properties, by knowing its id and setting it to the var thisItem in the DetailVC
I was going to use the indexPath approach until I came to understand that it would be unreliable/wrong in some situations (deleted or moved cell, for instance).
What I did is simpler. By example, I am displaying a series of colors and their RGB values—one per tableview cell. Each color is defined in an array of color structures. For clarity these are:
struct ColorStruct {
var colorname:String = ""
var red: Int = 0
var green: Int = 0
var blue: Int = 0
}
var colors:[ColorStruct] = [] // The color array
My prototype cell has a var to hold the actual index/key into my array:
class allListsCell: UITableViewCell {
#IBOutlet var cellColorView: UIView!
#IBOutlet var cellColorname: UILabel!
var colorIndex = Int() // ---> points directly back to colors[]
#IBAction func colorEditButton(_ sender: UIButton, forEvent event: UIEvent) {
print("colorEditButton: colors[] index:\(self.colorIndex), \(colors[self.colorIndex].colorname)")
}
}
This solution takes three lines of code, one in the prototype cell definition, the second in the logic that populates a new cell, and the the third in the IBAction function which is called when any cell's button is pressed.
Because I have effectively hidden the "key" (index) to the data in each cell AS I am populating that new cell, there is no calculation required -and- if you move cells there is no need to update anything.