CollectionViewCell: Access to data of a cell through indexPath? - swift

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

Related

Change colour of like button in tableview cell

I am trying to change the color of the like button in my table view cell to red when the users taps on the like button for a specific post. But at the moment when i click like for one post, all the other post like buttons turn red too.
func didTapLike(_ sender: UIButton) {
if let indexPath = getCurrentCellIndexPath(sender) {
clickedLike = indexPath
like()
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if indexPath.section == 0 {
let cell = tableView.dequeueReusableCell(withIdentifier: "postCell", for: indexPath) as! PostTableViewCell
if let indexPath = clickedLike {
// change like button to red
let selectedRow = indexPath.row
cell.HeartButton.setImage(UIImage(systemName: "heart.fill"), for: .normal)
cell.HeartButton.tintColor = UIColor.red
cell.LikeCount.textColor = UIColor.red
}
}
}
You have to understand the principle of reusable cells. What is happening is the cells that you have liked are being re-used to the cells that you never liked in the first place. Thus, it looks as if you liked everything because property of the cells that you never liked never had their properties reset.
The same cell maybe be reused with the same properties later on even though you have set its properties at the cellForRowAt function which is why you may see that there's no changes. Thus, you have to specifically reset its properties every time it is reused.
This can be done by overriding the prepareForReuse()inside the cell and setting the properties to its default values again. For example, you may want to try the following in your PostTableViewCell:
override func prepareForReuse() {
super.prepareForReuse()
//set it all back to the default values (in this case, heart and black color tint)
self.HeartButton.setImage(UIImage(systemName: "heart"), for: .normal)
self.HeartButton.tintColor = UIColor.black
self.LikeCount.textColor = UIColor.black
}

Swift Firestore, "Like" feature not working with button in table view cell

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.

Swift tableview cell doesn't save the state with different identifier

I am very confused on the reuse of the cells.
I have a table, each cell is a cell with a switch on it. If I toggle a switch I set the background color of that cell to a different color. However every time I scroll these changes don't persist.
I am subclassing UITalbeViewCell to create my own custom cell. Each cell has a different identifier. However when I scroll through the table, whatever changes I made to the cell still doesn't save. I've read similar questions but none of them worked.. Some suggested subclass which I did, some suggested use different identifier which I also did...
Here is the code of my tableview.
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let key = Array(dataSource[indexPath.section].keys)[indexPath.row]
let cell = CellWithSwitch.init(style: .subtitle, reuseIdentifier: key)
cell.awakeFromNib()
let val = Array(dataSource[indexPath.section].values)[indexPath.row]
cell.switchView?.addTarget(self, action: #selector(self.switchChanged(_:)), for: .valueChanged)
if let index = key.firstIndex(of: "."){
cell.textLabel?.text = String(key.suffix(from: key.index(index, offsetBy: 1)))
}else{
cell.textLabel?.text = key;
}
cell.switchView?.setOn(val, animated: true)
return cell
}
You can change array value in switchChange action
lets i take array for switch as below:
var arrSwitch = [false,false,false,false,false,false,false,false,false,false]
Below is my cellForRowAt Method
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "customCell") as! customCell
cell. switchView.setOn(self.arrSwitch[indexPath.row], animated: false)
cell. switchView.tag = indexPath.row
cell. switchView.addTarget(self, action: #selector(self.onSwitchTap(_:)), for: .valueChanged)
return cell
}
Here is my onSwitchTap Action
#IBAction func onSwitchTap(_ sender: UISwitch) {
self.arrSwitch[sender.tag] = !self.arrSwitch[sender.tag]
}
Now on scroll it will persist last changes you have done.

IOS TableView how can I update an element in my TableView

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.

Is it possible to retrieve data from collectionViewCell?

I'm looking to retrieve data, such as a label.text from within a visible cell, and I don't know how that would be possible.
Plain context: I have a collectionView displaying questions (each cell display a question)
the collectionView is within a UIView.
Within this same uiview I have a button which, when pressed, should validate the question answer.
At this point the only thing Im doing is linking a #IBAction to the button in order for it to println the question from the visible cell (only one cell is visible).
here is how I construct the cell
func numberOfSectionsInCollectionView(collectionView: UICollectionView) -> Int {
return 1
}
func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
// return arr.count
return questionData.count
}
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell:CollectionViewCell = collectionView.dequeueReusableCellWithReuseIdentifier("CollectionViewCell", forIndexPath: indexPath) as CollectionViewCell
let thereq:PFObject = self.questionData.objectAtIndex(indexPath.row) as PFObject
// cell.contentView.frame = cell.bounds
var action = thereq.objectForKey("action") as String
var what = thereq.objectForKey("what") as String
cell.askingQuestionLabel.text = "where \(action) \(what)"
return cell
}
on button touch (button is out of the cell).
I know visibleCells() exists but it doesn't look like I can retrieve info out of it.
for cell in self.collectionView!.visibleCells() as [UICollectionViewCell] {
}
Is there any way?
You already have a custom collection view cell class. The obvious solution is to pass the PFObject to the CollectionViewCell in your cellForItemAtIndexRow method, and then retrieve it from the cell when the button is clicked.
Just cast the return value from visibleCells to your collection view cell class and then pull the PFObject out of it.
To access data (such as a label.text) within a visible cell, you can use the visibleCells() method as follow:
for item in self.collectionView!.visibleCells() as [UICollectionViewCell] {
var indexpath : NSIndexPath = self.collectionView!.indexPathForCell(item as CollectionViewCell)!
var cell : CollectionViewCell = self.collectionView!.cellForItemAtIndexPath(indexpath) as CollectionViewCell
//access cell data
println(cell.labelName.text)
}