I'm trying to implement a like button function on my app, similar to facebook or instagram, through parse. I tried using this code below and it works. When the user taps the button on an object(or in my case messages), the like goes up by 1 point. However, when the user quits the app and launches it they can like the same object again, meaning they can like as many times as they want. Do I need to edit something in this code or try a whole different method?
#IBAction func likeButton(sender: UIButton) {
sender.enabled = false
sender.userInteractionEnabled = false
sender.alpha = 0.5
//get the point in the table view that corresponds to the button that was pressed
//in my case these were a bunch of cells each with their own like button
let hitPoint = sender.convertPoint(CGPointZero, toView: self.tableView)
let hitIndex = self.tableView.indexPathForRowAtPoint(hitPoint)
let object = objectAtIndexPath(hitIndex)
//this is where I incremented the key for the object
object!.incrementKey("count")
object!.saveInBackground()
self.tableView.reloadData()
NSLog("Top Index Path \(hitIndex?.row)")
}
Here is the way in which I would approach the problem:
First, I would make disabling the button into its own function like so:
func disableButton(button: UIButton){
button.enabled = false
button.userInteractionEnabled = false
button.alpha = 0.5
}
(And when the user presses the like button it is disabled like so:)
#IBAction func likeButton(sender: UIButton) {
disableButton(sender)
//get the point in the table view that corresponds to the button that was pressed
//in my case these were a bunch of cells each with their own like button
let hitPoint = sender.convertPoint(CGPointZero, toView: self.tableView)
let hitIndex = self.tableView.indexPathForRowAtPoint(hitPoint)
let object = objectAtIndexPath(hitIndex)
//this is where I incremented the key for the object
object!.incrementKey("count")
object!.saveInBackground()
self.tableView.reloadData()
NSLog("Top Index Path \(hitIndex?.row)")
}
Then I would make an attribute of users their liked posts, which is an array of strings of the objectIds of these posts. (Since users would probably end up liking many posts, you really should use relations, but arrays are easier to understand. The documentation for relationships between PFObjects is here.) Then, when creating the cell for each post, where cell is the cell you are creating, post is the current post in the list and cell.likeButton is the like button from the cell:
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell{
let cell = YourCell()
let post = posts[indexPath.row]
if (PFUser.currentUser!["likedPosts"] as! [String]).contains(post.objectId){
cell.disableButton(cell.likeButton)
}
//Setup rest of cell
Related
In my swift app I have a UITableView with one static cell and many dynamic cells.
Static cell contains different fields, such as labels, map (taken from MapKit) and a button, that indicates whether user voted up or not.
Now, when user presses the button, I want to change its color, possibly without refreshing anything else.
So far my code looks like this:
var currentUserVote:Int = 0
func tableView(_ tview: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if (indexPath as NSIndexPath).row == 0 {
let cell = tview.dequeueReusableCell(withIdentifier: "cellStatic") as! VideoDetailsCell
fetchScore(cell.score)
let voteUpImage = UIImage(named: "voteUp");
let tintedVoteUpImage = voteUpImage?.withRenderingMode(UIImageRenderingMode.alwaysTemplate)
cell.voteUpButton.setImage(tintedVoteUpImage, for: UIControlState())
checkUsersVote() { responseObject in
if(responseObject == 1) {
cell.voteUpButton.tintColor = orangeColor
} else if (responseObject == -1){
cell.voteUpButton.tintColor = greyColor
} else {
cell.voteUpButton.tintColor = greyColor
}
self.currentUserVote = responseObject
}
//map handling:
let regionRadius: CLLocationDistance = 1000
let initialLocation = CLLocation(latitude: latitude, longitude: longitude)
centerMapOnLocation(initialLocation, map: cell.mapView, radius: regionRadius)
//cell.mapView.isScrollEnabled = false
cell.mapView.delegate = self
.
.
.
return cell
} else {
//handle dynamic cells
}
}
So in the method above I'm checking if user voted already and based on that I'm setting different color on the button. I'm also centering the map on a specific point.
Now, since it's a static cell, I connected IBAction outlet to that button:
#IBAction func voteUpButtonAction(_ sender: AnyObject) {
if(currentUserVote == 1) {
self.vote(0)
}else if (currentUserVote == -1){
self.vote(1)
} else {
self.vote(1)
}
}
and the vote method works as follows:
func vote(_ vote: Int){
let indexPath = IndexPath(row: 0, section: 0)
let cell = tview.dequeueReusableCell(withIdentifier: "cellStatic") as! VideoDetailsCell
switch(vote) {
case 1:
cell.voteUpButton.tintColor = orangeColor
case 0:
cell.voteUpButton.tintColor = greyColor
case -1:
cell.voteUpButton.tintColor = greyColor
default:
cell.voteUpButton.tintColor = greyColor
}
tview.beginUpdates()
tview.reloadRows(at: [indexPath], with: .automatic)
tview.endUpdates()
currentUserVote = vote
//sending vote to my backend
}
My problem is, that when user taps the button, he invokes the method vote, then - based on the vote, the button changes color, but immediately after that method cellForRow is called and it changes the color of the button again. Also, it refreshes the map that's inside of it.
What I want to achieve is that when user taps the button, it should immediately change its color and that's it. Map stays untouched and the button is not changed again from cellForRow.
Is there a way of refreshing only that particular button without calling again cellForRow?
First of all, you confuse static and dynamic cells. You can use static cells only in the UITableViewController and you can't use static and dynamic cell at the same time.
I strongly recommend you not to use cell for storing map and button. All elements from the cell will be released after scrolling it beyond the screen.
I can advise you use TableViewHeaderView for this task. In this case you will be able set button and map view as #IBOutlet.
(See more about adding tableview headerView. You can also set it from interface builder.)
Another way is change tableView.contentInset and set your view with map and button as subview to tableView. This method is used when you need create Stretchy Headers.
It should be quite easy, simply do it in your button handler. The sender argument there is the button object that caused the action. When you were connecting it from IB there was a dropdown to select sender type, you may have missed it and the whole thing would have been obvious with UIButton type there.
Simply change your handler like this :
#IBAction func voteUpButtonAction(_ sender: UIButton) {
if(currentUserVote == 1) {
self.vote(0)
}else if (currentUserVote == -1){
self.vote(1)
} else {
self.vote(1)
}
sender.backgroundColor = yourFavouriteColor
}
Another approach would be to create an IBOutlet for your button, since its from a static cell, and then you would be able to reference it from any place in your view controller.
In this call:
func tableView(_ tview: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
I see it calls checkUsersVote() which I'm guessing should get the updated value set in the vote() call. Could the problem be that you aren't doing this
currentUserVote = vote
until after reloadRows() is called?
I'm really close to figuring this out. So in the iOS mail app when you click on the two arrow keys it takes you to the previous/next mail. Its on the top right
I've managed to pass the indexPath value to my second viewcontroller and print in in the console. I can also increase and decrease from it.
if segue.identifier == "DetailVC" {
let detailVC = segue.destination as! DetailVC
let indexPath = self.collectionViewIBO.indexPathsForSelectedItems?.last!
detailVC.index = indexPath
}
EDIT
This is where I'm pulling the data from. It reads the values from my model. I cannot assign an indexPath to it however. I can only do that from the previous view controller
var monster: Monsters!
I've attempted to implement the "previous" functionality using this code. My view styling are in the displayDataForIndexPath() function and the function is called from my view will appear
#IBAction func monsPreviousIBO(_ sender: Any) {
self.index = IndexPath(row: self.index.row - 1, section: self.index.section)
displayDataForIndexPath()
}
But all it does is decrease the IndexPath. For some reason the data doesn't actually reload with my function. I'm missing some important puzzle piece here to achieving the same functionality.
EDIT The code in my displayDataForIndex is as follows
func displayDataForIndexPath() {
if index.row == 0 {
self.monsPreviousIBO.removeFromSuperview()
}
var monsterName = (String(format: "%03d", monster.speciesId!))
self.navigationItem.title = monster.name!
let gif = UIImage(gifName: monsterName)
self.gifIBO.setGifImage(gif, manager: gifManager)
gifIBO.contentMode = .center
guard monster.legendary! != true else {
// Value requirements not met, do something
monsterStatusLegend()
return
}
guard monster.subLegend! != true else {
// Value requirements not met, do something
monsterStatusSub()
return
}
guard monster.isMega! != true else {
// Value requirements not met, do something
monsterStatusMega()
return
}
}
you display all data depending on monster but you never change the monster depending which indexPath you used.
add some code to populate the monster from indexPath
monster = getMonster(index.row)
or in your case
#IBAction func monsPreviousIBO(_ sender: Any) {
self.index = IndexPath(row: self.index.row - 1, section: self.index.section)
monster = mons[self.index.row]
displayDataForIndexPath()
}
or better in the displayDataForIndexPath() add this line:
func displayDataForIndexPath() {
if index.row == 0 {
self.monsPreviousIBO.removeFromSuperview()
}
monster = mons[self.index.row]
//....
NOTE some suggestions:
i would change the line for the button then it is enabled if the indexpath gets >0:
self.monsPreviousIBO.isEnabled = (index.row != 0)
just save the row as monsterIndex = indexPath.row and then deal only with the index and not indexPath.
you don't need to save the current monster as monster if you use the monster only in displayDataForIndexPath - then you can get the current monster just there and have it a local variable in this function:
var monster = mons[self.index.row]
I tried to copy only the necessary code to show my problem. I have a tableview with dynamic content. I created a prototype cell and it has a user name and 10 stars (it's a rating page). People in the group are allowed to rate other people. Everything is working ok, but I have a problem when I scroll down. If I rate my first user with 8 stars, when I scroll down then some user that was in the bottom area of the tableview, appears with the rate that I gave to my first user. I know that tableview reuse cells. I tried many things but with no success. Hope someone can help me on that.
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let model = users[indexPath.row]
let cell = tableView.dequeueReusableCellWithIdentifier("RatingCell") as! RatingTableViewCell
cell.tag = indexPath.row
cell.playerLabel.text = model.name
cell.averageView.layer.borderWidth = 1
cell.averageView.layer.borderColor = Color.Gray1.CGColor
cell.averageView.layer.cornerRadius = 5
cell.starsView.userInteractionEnabled = true
cell.averageLabel.text = "\(user.grade)"
for i in 0...9 {
let star = cell.starsView.subviews[i] as! UIImageView
star.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(starTap)))
star.userInteractionEnabled = true
star.tag = i
star.image = UIImage(named: (i + 1 <= grade ? "star-selected" : "star-empty"))
}
return cell
}
func changeRating(sender: UIImageView) {
let selectedStarIndex = sender.tag
let cell = sender.superview?.superview?.superview as! RatingTableViewCell
let model = users[cell.tag]
let stars = sender.superview?.subviews as! [UIImageView]
cell.averageLabel.text = "\(selectedStarIndex + 1)"
for i in 0...9 {
let imgName = i <= selectedStarIndex ? "star-selected" : "star-empty"
stars[i].image = UIImage(named: imgName)
}
}
func starTap(gesture: UITapGestureRecognizer) {
changeRating(gesture.view as! UIImageView)
}
The way to solve this problem is by updating the model that holds all the information for the uitableviewcell. Whenever a rating is updated fora particular cell, make sure you reflect that update in the respective object / dictionary in an array. Furthermore, if you have a customuitableviewcell, it might be a good idea to reset the stars in the "prepareForUse" function, so that way when a cell is reused it doesn't use old data.
In your comments, you said that you have an array with selected rates.But you did not show that in your code.
In my opinion, you need record indexPath too, because indexPath.row is binding with your rate data(may be grade?).The best way to do so is that #Jay described up.And you should not write the code of configuring cell data and cell's logic in your view controller.If your business logic is complex, you will find that it is a nightmare.^=^
I have a PFTableViewController with PFTableViewCells in Swift.
In each TableViewCell (with a height of 80% the screen size), there is a button. I would like that when a user select the button, there is an automatic scroll to the next TableViewCell.
Any idea how I can make it?
Thanks a lot
Just determine the indexPath and scroll to the next indexPath. This example assumes a flat table without sections.
func buttonTapped(sender: UIButton) {
let point = sender.convertPoint(CGPointZero, toView:tableView)
let indexPath = tableView.indexPathForRowAtPoint(point)!
// check for last item in table
if indexPath.row < tableView.numberOfRowsInSection(indexPath.section) {
let newIndexPath = NSIndexPath(forRow:indexPath.row + 1 inSection:indexPath.section)
tableView.scrollToRowAtIndexPath(
newIndexPath, atScrollPosition:.Top, animated: true)
}
}
i have a TableView with a customCell. I set the values of the customCell Elements inside cellForRowAtIndexPath:. No problem. But i want to change some customCell Element Values outside of the cellForRowAtIndexPath: Scope. For example after a Swipe i want to change the Value of a cell element inside my swipe function
func tableView(tableView: UITableView!, cellForRowAtIndexPath indexPath: NSIndexPath!) -> UITableViewCell! {
let cell:customCell = self.tableView?.dequeueReusableCellWithIdentifier("customCell")! as customCell
let rowData: NSDictionary = self.tableData[indexPath.row] as NSDictionary
let imageSwipeLeft = UISwipeGestureRecognizer(target: self, action: "imageSwiped:")
imageSwipeLeft.direction = .Left
let urlString: NSString = rowData["testImage"] as NSString
self.indexPathArray += [indexPath]
cell.testLabel.text = "Test Label"
cell.testImage.image = image
cell.testImage.tag = indexPath.row
cell.testImage.addGestureRecognizer(imageSwipeLeft)
cell.testImage.userInteractionEnabled = true
ImageLoader.sharedLoader.imageForUrl(urlString, completionHandler:{(image: UIImage?, url: String) in
cell.testImage.image = image
cell.testImage.layer.borderWidth = 6;
cell.testImage.layer.borderColor = UIColor.whiteColor().CGColor
cell.testImage.clipsToBounds = true
cell.placeholderLoading.stopAnimating()
})
return cell
}
func imageSwiped(recognizer: UISwipeGestureRecognizer) {
let testData: NSDictionary = self.tableData[recognizer.view.tag] as NSDictionary
let imageSlide = recognizer.view as UIImageView
var imageURL = testData["image"] as String
ImageLoader.sharedLoader.imageForUrl(imageURL, completionHandler:{(image: UIImage?, url: String) in
UIView.transitionWithView(imageSlide,
duration:0.44,
options: .TransitionCrossDissolve,
animations: { imageSlide.image = image },
completion: nil)
})
let indexPath = self.indexPathArray[recognizer.view.tag] as NSIndexPath
let cell = self.tableView?.cellForRowAtIndexPath(indexPath)as customCell
cell.testLabel.text = "Test"
}
UITableView follows the Model-View-Controller pattern. According to this pattern, all your changes need to be done to the model - in other words, to the data structure that stores the information from which you populate your cell data. Once you made the change to the model, you tell the view that the data has changed, which would then show the new data.
Let's say that your cellForRowAtIndexPath function reads from an array. Your imageSwiped function should then locate the item that has been swiped, modify its entry in the array, and call either reloadData or reloadRowsAtIndexPaths.
That's it! Once you notify the table view of the reload, it would go back to the array, find the modified data, and call your cellForRowAtIndexPath to display it.
Specifically, in your code add an array called swipedCells to the same class where you declared tableData array:
var swipedCells = Boolean[](count:self.tableData.count, repeatedValue: false)
Now replace
cell.testLabel.text = "Test Label"
line with
if self.swipedCells[indexPath.row] {
cell.testLabel.text = "Swiped!"
} else {
cell.testLabel.text = "Test Label"
}
Finally, change the imageSwiped as follows:
let indexPathRow = self.indexPathArray[recognizer.view.tag] as Int
self.swipedCells[indexPathRow] = true
self.tableView?.reloadData()
This way the cells that you have swiped would continue to have a label "Swiped!" even after you scroll.
Ok after a huge amount of hours :D i solved my problem.
let indexPath = self.priceObjects[recognizer.view.tag] as NSIndexPath
let cell = self.tableView?.cellForRowAtIndexPath(indexPath)as customCell
i call this inside my imageSwipe function. Inside the cellForRowAtIndexPath function where i set the initial cell element values i store the indexPath inside an array. An i tag the Images to get a Index if the Swipe Events happens. I think it is far away from the ideal way...but works for me. I hope someday someone post the good way ;) cause this is very quick n' dirty.