I'm using a UISwipeGestureRecognizer to detect a swipe for a cell in a UITableViewCell, similar to THIS LINK which will allow the user to 'Like' a photo.
The problem is that I dont quite understand how to change the Like value for that specific post - and it doesn't have an indexPath like other 'built-in' methods. I also don't understand how it knows to use the cell that is showing predominantly on the screen, since there might be more than one cell that has not yet been "dequeued"?:
#objc func mySwipeAction (swipe: UISwipeGestureRecognizer) {
switch swipe.direction.rawValue {
case 1:
print ("the PostID you selected to LIKE is ...")
case 2:
print ("the PostID you selected to Undo your LIKE is ...")
default:
break
}
}
and my tableView looks like this:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "postTopContributions", for: indexPath) as! PostTopContributions
let postImage = postImageArray [indexPath.row]
let imageURL = postImage.postImageURL
cell.delegate = self
cell.postSingleImage.loadImageUsingCacheWithUrlString(imageURL)
cell.postSingleLikes.text = "\(postImageArray [indexPath.row].contributionPhotoLikes)"
cell.postSingleImage.isUserInteractionEnabled = true
let leftSwipe = UISwipeGestureRecognizer(target: self, action: #selector(self.mySwipeAction(swipe:)))
let rightSwipe = UISwipeGestureRecognizer(target: self, action: #selector(self.mySwipeAction(swipe:)))
leftSwipe.direction = UISwipeGestureRecognizerDirection.left
rightSwipe.direction = UISwipeGestureRecognizerDirection.right
cell.postSingleImage.addGestureRecognizer(leftSwipe)
cell.postSingleImage.addGestureRecognizer(rightSwipe)
let selectedCell = self.postImageArray [indexPath.row]
return cell
}
I don't want to use the native TableView row swipe left to delete methods - for various UX purposes in this specific case.
You can try
cell.postSingleImage.addGestureRecognizer(leftSwipe)
cell.postSingleImage.addGestureRecognizer(rightSwipe)
cell.postSingleImage.tag = indexPath.row
Don't recommend adding gestures inside cellForRowAt , you may add
them inside init for programmatic cells or awakeFromNib for xib /
prototype cells
#objc func mySwipeAction (swipe: UISwipeGestureRecognizer) {
let index = swipe.view.tag
let selectedCell = self.postImageArray[index]
switch swipe.direction.rawValue {
case 1:
print ("the PostID you selected to LIKE is ...")
// edit dataSource array
case 2:
print ("the PostID you selected to Undo your LIKE is ...")
// edit dataSource array
default:
break
// reload table IndexPath
}
}
You can pass your indexpath as a parameter in your selector. and then add the like at yourArray[indexpath.row]
You can set the tag of the cell image that you are adding the GestureRecognizer to the indexPath row of the cell itself:
cell.postSingleImage.tag = indexPath.row
cell.postSingleImage.addGestureRecognizer(leftSwipe)
cell.postSingleImage.addGestureRecognizer(rightSwipe)
Then, you can determine which cell triggered the GestureRecognizer by getting the view's tag that triggered the swipe gesture:
#objc func mySwipeAction (gesture: UISwipeGestureRecognizer) {
let indexPathRow = gesture.view.tag
let indexPath = IndexPath(row: indexPathRow, section: 0) // assuming this is a 1 column table not a collection view
if let cell = tableView.cellForRow(at: indexPath) as? PostTopContributions {
// ... and then do what you would like with the PostTopContributions cell object
print ("the PostID you selected to LIKE is ... " + cell.id)
}
}
Hope that this helped!
Related
I am trying to implement a "Like" functionality to my app. I have a "recipes" with a field in each called "likes". In each table view cell I have a "heart" button which when the user clicks it should increment the likes by 1. I have got it to the point it increments the field but not for the correct recipe. for example if I click the like button on the first recipe it might increment the likes field for the 3rd recipe. Can anyone help with incrementing the correct document.
//Function for incrementing likes
#objc func likesPressed() {
if User.currentUser() != nil{
FirebaseReference(.RecipePosts).document(recipeToLike.id).updateData([
"likes": FieldValue.increment(Int64(1))
])
self.hud.textLabel.text = ""
self.hud.indicatorView = JGProgressHUDImageIndicatorView(image: UIImage(systemName: "heart.fill")!)
self.hud.show(in: self.view)
self.hud.dismiss(afterDelay: 2.0)
}else{
self.hud.textLabel.text = "Please Login To Your Account!"
self.hud.indicatorView = JGProgressHUDErrorIndicatorView()
self.hud.show(in: self.view)
self.hud.dismiss(afterDelay: 3.0)
}
}
//Table View cell for row at
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = LatestRecipeTV.dequeueReusableCell(withIdentifier: "latestCell", for: indexPath) as! SocialFeedTableViewCell
cell.configureCell(recipeArray[indexPath.row])
cell.cellDelegate? = self
//Getting the recipe to like
recipeToLike = recipeArray[indexPath.row]
//"Like" Button action in Cell
cell.likesBtn.addTarget(self, action: #selector(likesPressed), for: .touchUpInside)
return cell
}
Right now, your recipeToLike is going to get set many times while the table is populated with data. This is because cellForRowAt gets called when the cell gets created -- not when the button is tapped.
In order to figure out which recipe ID you should be sending, you'll need to have a way to figure out which button was tapped. A common way to do this is by assigning a tag to the button.
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = LatestRecipeTV.dequeueReusableCell(withIdentifier: "latestCell", for: indexPath) as! SocialFeedTableViewCell
cell.configureCell(recipeArray[indexPath.row])
cell.cellDelegate? = self
cell.likesBtn.tag = indexPath.row //<-- Here
//"Like" Button action in Cell
cell.likesBtn.addTarget(self, action: #selector(likesPressed), for: .touchUpInside)
return cell
}
Then, in your likesPressed function, you'll need to read that tag:
#objc func likesPressed(sender: UIButton) { //<-- Here
let recipeToLike = recipeArray[sender.tag] //<-- Here
if User.currentUser() != nil{
FirebaseReference(.RecipePosts).document(recipeToLike.id).updateData([
"likes": FieldValue.increment(Int64(1))
])
self.hud.textLabel.text = ""
self.hud.indicatorView = JGProgressHUDImageIndicatorView(image: UIImage(systemName: "heart.fill")!)
self.hud.show(in: self.view)
self.hud.dismiss(afterDelay: 2.0)
}else{
self.hud.textLabel.text = "Please Login To Your Account!"
self.hud.indicatorView = JGProgressHUDErrorIndicatorView()
self.hud.show(in: self.view)
self.hud.dismiss(afterDelay: 3.0)
}
}
Since you'll be defining recipeToLike locally within likesPressed, you can get rid of the original recipeToLike property on your class.
What I have:
I have a CollectionViewCell as .xib + .swift files.
Every cell gets their data from the database.
Inside every cell I have a like button.
What I want:
When I press the like button of a certain cell, I want to be able to read this data so I can change it and write the new data in the Database. So I want to change the like attribute of the dataset of a certain cell and save it in the DB
What I tried:
I have the indexPath of the cell but how can I read the data of the cell?
#IBAction func likeButton(_ sender: UIButton) {
var superview = self.superview as! UICollectionView
let buttonPosition:CGPoint = sender.convert(CGPoint.zero, to:superview)
let indexPath = superview.indexPathForItem(at: buttonPosition)
print(superview.cellForItem(at: indexPath!))
// Change picture
if sender.currentImage == UIImage(systemName: "heart.fill") {
sender.setImage(UIImage(systemName: "heart"), for: .normal)
} else {
sender.setImage(UIImage(systemName: "heart.fill"), for: .normal)
}
}
UICollectionViewDataSource
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: MyCollectionViewCell.identifier,
for: indexPath) as! MyCollectionViewCell
cell.backgroundColor = .white
let newShoeCell = shoesArray?.randomElement()
// Fill cells with data
cell.imageView.image = UIImage(named: newShoeCell!.imgName)
cell.shoeTitle.text = newShoeCell!.title
cell.price.text = String(newShoeCell!.price)
cell.ratingNumberLabel.text = String(newShoeCell!.meanRating)
cell.floatRatingView.rating = newShoeCell!.meanRating
if newShoeCell!.liked {
cell.likeButtonOutlet.setImage(UIImage(systemName: "heart.fill"), for: .normal)
} else {
cell.likeButtonOutlet.setImage(UIImage(systemName: "heart"), for: .normal)
}
return cell
}
You need to change your thinking. It is not the data "of a cell" A cell is a view object. It displays information from your data model to the user, and collects input from the user.
You asked "...how can I read the data of the cell?" Short answer: Don't. You should be saving changes into your data model as you go, so once you have an index path, you should use it to index into your data model and get the data from there.
You need to figure out which IndexPath the tapped button belongs to, and fetch the data for that IndexPath from your data model.
If you look at my answer on this thread I show an extension to UITableView that lets you figure out which IndexPath contains a button. Almost the exact same appraoch should work for collection views. The idea is simple:
In the button action, get the coordinates of the button's frame.
Ask the owning table/collection view to convert those coordinates to
an index path
Use that index path to fetch your data.
The only difference is that for collection views, the method you use to figure out which IndexPath the button maps to is indexPathForItem(at:) instead of indexPathForRow(at:)
I suggest you add a property to your cell
class MyCell: UITableViewCell {
var tapAction: (() -> Void)?
...
#IBAction func likeButton(_ sender: UIButton) {
tapAction?()
...
}
}
and set it in view controller
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
...
cell.tapAction = { [weak self] in
// call to database or whatever here and then reload cell
tableView.reloadRows(at: [indexPath], with: .automatic)
...
}
In general, cell should never care what its indexPath is, nor should it make calls to superview
This is not a massive issue with me, I am just struggling to understand what is happening. Other rather how to make it work the way I want it to work.
Consider the following code of any standard UITableViewController:
var tapGestureRecognizer = UITapGestureRecognizer()
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let customCell = tableView.dequeueReusableCell(withIdentifier: customCellID) as? CustomTableViewCell else { return UITableViewCell() }
if indexPath.row == 0 {
print("Inside cellForRowAt: \(indexPath.row)")
customCell.backgroundColor = .red
tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(tagIndexPathRowMethod))
tapGestureRecognizer.cancelsTouchesInView = false
tapGestureRecognizer.view?.tag = indexPath.row
customCell.isUserInteractionEnabled = true
customCell.addGestureRecognizer(tapGestureRecognizer)
return customCell
} else {
print("Inside cellForRowAt: \(indexPath.row)")
customCell.backgroundColor = .blue
tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(tagIndexPathRowMethod))
tapGestureRecognizer.cancelsTouchesInView = false
tapGestureRecognizer.view?.tag = indexPath.row
customCell.isUserInteractionEnabled = true
customCell.addGestureRecognizer(tapGestureRecognizer)
return customCell
}
}
#objc private func tagIndexPathRowMethod(sender: UITapGestureRecognizer) {
print("Cell number tag: \(String(describing: sender.view?.tag))")
}
I've already tried splitting the properties, methods and cells into separate codes such as
var firstTapGestureRecognizer = UITapGestureRecognizer()
var secondTapGestureRecognizer = UITapGestureRecognizer()
etc, but the tag still prints only a 0 from both cells.
Could someone explain to me how to make the print statement in tagIndexPathRowMethod returns 0 as tag no matter if I tap in cell 0 or cell 1, but the print statements inside cellForRowAt prints the correct indexPath.row integers, 0 and 1? I know I could use didSelectRowAt, but I've just become stubborn I guess.
(I'm well aware of all times I'm breaking with the DRY principle, but it just serves as a pedagogical example.)
Updated answer
This happening because the you setting tag before adding gestures to the cell. In this case, tapGestureRecognizer.view is null at that time. Just do one thing set tag after adding gestures to the cell.
customCell.addGestureRecognizer(tapGestureRecognizer)
tapGestureRecognizer.view?.tag = indexPath.row
You need to set the value of view tag inside the UITapGestureRecognizer class. Just add the line below after initializing 'customCell'.
customCell.tag = indexPath.row
Code:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let customCell = tableView.dequeueReusableCell(withIdentifier: customCellID) as? CustomTableViewCell else { return UITableViewCell() }
customCell.tag = indexPath.row
if indexPath.row == 0 {
print("Inside cellForRowAt: \(indexPath.row)")
customCell.backgroundColor = .red
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(tagIndexPathRowMethod))
tapGestureRecognizer.cancelsTouchesInView = false
tapGestureRecognizer.view?.tag = indexPath.row
customCell.isUserInteractionEnabled = true
customCell.addGestureRecognizer(tapGestureRecognizer)
return customCell
} else {
print("Inside cellForRowAt: \(indexPath.row)")
customCell.backgroundColor = .blue
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(tagIndexPathRowMethod))
tapGestureRecognizer.cancelsTouchesInView = false
tapGestureRecognizer.view?.tag = indexPath.row
customCell.isUserInteractionEnabled = true
customCell.addGestureRecognizer(tapGestureRecognizer)
return customCell
}
}
#objc private func tagIndexPathRowMethod(sender: UITapGestureRecognizer) {
print("Cell number tag: \(String(describing: sender.view?.tag))")
}
You should not use cellForRow to add tapGesture to the cell ... as cell get reuse so same gesture will apply to multiple cells. So instead of adding it in cell for row .. add them in Custom cell init() method in your case in CustomTableViewCell class so it adds only once ... you can set Tag to that gesture in cellForRow method that will not cause any issue ...
I am using Swift 4 and I finally got my TableView cell working correctly . I have a like button and when a user clicks it I update only the cell that was clicked and show a new image either like or unliked, in addition I show a LikeCounter +1 for like and -1 for unlike .That's all working.
My question is how can I make it so that I only update those 2 elements on button clicked ?
right now it updates the Table View Cell . This is my code
#IBAction func voteAction(_ sender: UIButton)
{
if let indexPathm = TableView.indexPathForView(sender) {
if streamsModel.VoteStatus[sender.tag] == "0" {
streamsModel.VoteStatus[sender.tag] = "1"
streamsModel.Votes[sender.tag] = streamsModel.Votes[sender.tag] + 1
} else {
streamsModel.VoteStatus[sender.tag] = "0"
streamsModel.Votes[sender.tag] = streamsModel.Votes[sender.tag] - 1
}
TableView.reloadRows(at: [indexPathm],with: .none)
}
}
Once you click that button then it goes here
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "HomeTVC", for: indexPath) as! HomeTVC
cell.vote_status = streamsModel.VoteStatus[indexPath.row]
if streamsModel.VoteStatus[indexPath.row] != "0" {
cell.voteImage.setImage(UIImage(named: "liked"), for: .normal)
} else {
cell.voteImage.setImage(UIImage(named: "unliked"), for: .normal) }
}
I have a lot more cell elements in that TableViewCell however I would only like to update and isolate that specific piece of code in my TableViewCell .
I have tried doing it using this method Update label in custom UITableView Cell however I find that if I scroll down and then back up it reverts whatever changes I did .
// This works however if you scroll down then back up
// it reverts back to the original status
let button = sender as! UIButton
let view = button.superview!
let cell = view.superview as! HomeTVC
let indexPath = tableView.indexPathForCell(cell)
println(indexPath)
if(indexPath != nil){
if streamsModel.VoteStatus[indexPath.row] != "0" {
cell.voteImage.setImage(UIImage(named: "liked"), for: .normal)
} else {
cell.voteImage.setImage(UIImage(named: "unliked"), for: .normal) }
}
}
So now I'm calling but it's updating the whole cell .
TableView.reloadRows(at: [indexPathm],with: .none)
You can retrieve the cell and update only the properties of the cell directly like this:
if let cell = tableView.cellForRow(at: indexPath) as? HomeTVC {
// Update the cell
}
EDIT:
I missed the second part of the question, sorry. I think that the problem here is that you're not updating your model, so when you scroll up the delegate calls tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) and sets the value as saved previously on the model. You have to update the value of streamsModel.VoteStatus when the button is pressed.
I have an UITableView populated by a cellForRowAtIndexPath:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var cell: UITableViewCell = self.tableView.dequeueReusableCellWithIdentifier("customTableViewCell") as! UITableViewCell
let task = frc.objectAtIndexPath(indexPath) as! Task
cell.textLabel?.text = task.summary
var detail = task.detail
var context = task.context
var due = task.date
var status = task.status
var responsible = task.responsable
var folder = task.folder
cell.detailTextLabel?.text = "Contexte: \(context), Detail: \(detail), Status: \(status), Ending date: \(due)"
return cell
}
On the storyboard, I have made a segue when clicking one cell of the tableView to open a detailViewController
this is my didSelectRowAtIndexPath:
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
let cell = tableView.cellForRowAtIndexPath(indexPath)
self.name = cell!.textLabel!.text!
println(self.name)
self.performSegueWithIdentifier("Show Detail", sender: indexPath);
}
and the prepareForSegue:
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject!) {
if let identifier = segue.identifier{
switch identifier {
case "Show Detail":
let indexPath = self.tableView.indexPathForSelectedRow()
let editTaskVC = segue.destinationViewController as! EditTaskViewController
editTaskVC.Name = "cell.textLabel?.text is what I would like to.."
default: break
}
}
}
If I do editTaskVC.Name = indexPath?.description I can see the description of the cell clicked like, <NSIndexPath: 0x78f96ab0>... for example.
Is it possible, instead of printing the description of the indexPath, printing the cell.textLabel?.text of the clicked row?
I have seen many, many tutorials or posts on forum but I haven't succeed to solve my problem...
Thank you for your help.
Regards.
Your intention is to pass along the cell.textLabel?.text to the destination view controller right?
You're taking a needless detour. The sender parameter in performSegueWithIdentifier: can take in an AnyObject, so you can go right ahead and pass it the name.
self.performSegueWithIdentifier("Show Detail", sender: name)
That way, prepareForSegue will have the item you need to pass along to the next view controller. Simply assign editTaskVC = sender as! String and you're good to go.
The piece of knowledge you were missing is that, the sender parameter in performSegueWithIdentifier: sender will automatically pass the sender's contents into prepareForSegue, as the sender parameter.
Since you already have the index path, you can simply invoke the table's cellForRowAtIndexPath to obtain the cell:
if let indexPath = self.tableView.indexPathForSelectedRow() {
if let cell = self.tableView.cellForRowAtIndexPath(indexPath) as? UITableViewCell {
let editTaskVC = segue.destinationViewController as! EditTaskViewController
editTaskVC.Name = cell.textLabel?.text
}
}
The indexPathForSelectedRow returns nil in 2 cases only:
if the index is out of range
if the cell is not visible