0

I have a UITapGestureRecognizer on a label as:

cell.label.userInteractionEnabled = true
let gestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(ViewController.labelTapped))
cell.label.addGestureRecognizer(gestureRecognizer)

I’d want it to change text when a user taps on it. After hours of looking for an answer on Stackoverflow, I’ve reached at a point where I could change text of a label. But it also changes text on other cell labels. Here’s what I implemented:

 func labelTapped(recognizer: UIGestureRecognizer){
        print("Label tapped.")

        let position: CGPoint =  recognizer.locationInView(self.tableView)
        let indexPath: NSIndexPath = self.tableView.indexPathForRowAtPoint(position)!
        let cell = self.tableView.cellForRowAtIndexPath(indexPath) as! DataTableViewCell
        
        cell.label.text = "Test."
    }

Help me find a solution.

Daniel
  • 20,420
  • 10
  • 92
  • 149
waseefakhtar
  • 1,373
  • 2
  • 24
  • 47
  • Have you tried adding the sender parameter to the gesture recognizer target action? You can check if the sender is a UILabel and only modify that instance. – Emptyless Jan 27 '17 at 19:52
  • iOS cells are reusable. You have to check whether or not the label has been tapped when displaying each cell. – Caleb Jan 27 '17 at 19:55
  • @Emptyless I believe `recognizer: UIGestureRecognizer` works as a sender. But how do I check if the sender is a UILabel? And do you mean there’s no use of `cell` and `indexPath` to detect the specific cell here? – waseefakhtar Jan 27 '17 at 19:55
  • Does this work for you: http://stackoverflow.com/a/30972323/4503593 – Emptyless Jan 27 '17 at 19:57
  • @Caleb which method do I check it in? would `cellForRowAtIndexPath` be fine? If so, how do I detect if the label has been tapped in `cellForRowAtIndexPath`? – waseefakhtar Jan 27 '17 at 19:58
  • @Emptyless it hasn’t. The “Test.” text applies to every other cell in the table. – waseefakhtar Jan 27 '17 at 20:02
  • Yes, you should check in `cellForRowAtIndexPath` and set the label to the correct text if you keep an array of indexes where the label has been tapped [like this](http://stackoverflow.com/a/31977748/5181636). You can also create a boolean in your `UITableViewCell` subclass and set the text inside of the subclass instead of `cellForRowAtIndexPath`. – Caleb Jan 27 '17 at 20:04
  • @Caleb let me try this and get back to you. – waseefakhtar Jan 27 '17 at 20:09
  • @Caleb I’ve been trying to implement the post you linked me to. I just need to get my UILabel working and test it but I’m stuck getting the `sender.tag` for the label the user taps on. I believe `sender.tag` only works on UIButton. But there must be an alternative. – waseefakhtar Jan 27 '17 at 21:11

3 Answers3

3

I would recommend changing the DataSource as Sneak already mentioned. You can then reload your tableView at specified IndexPaths to get desired result:

func handleLabelTap(sender: UIGestureRecognizer) {
    // Get references
    guard let label = sender.view as? UILabel else {
        print("Error not a label")
        return
    }
    let position = sender.location(in: self.tableView)
    guard let index = self.tableView.indexPathForRow(at: position) else { 
        print("Error label not in tableView")
        return
     }

    // Notify TableView of pending updates
    self.tableView.beginUpdates()
    defer { self.tableView.endUpdates() }

    // Alter Datasource
    self.data[index.row] = "Changed Value"
    self.tableView.reloadRows(at: [index], with: .automatic)
}

Where self.data is an array of String which represents your DataSource.

Edit: Swift 2.3 version as suggested in comments.

func handleLabelTap(sender: UIGestureRecognizer) {
    // Get references
    guard let label = sender.view as? UILabel else {
        print("Error not a label")
        return
    } 
    let position = sender.locationInView(self.tableView) 
    guard let index = self.tableView.indexPathForRowAtPoint(position) else { 
        print("Error label not in tableView")
        return
    } 

    // Notify TableView of pending updates
    self.tableView.beginUpdates() 
    defer { self.tableView.endUpdates() } 

    // Alter DataSource
    self.data[index.row] = "Changed Value" 
    self.tableView.reloadRowsAtIndexPaths([index], withRowAnimation: .Automatic)
}
zekel
  • 9,227
  • 10
  • 65
  • 96
Emptyless
  • 2,964
  • 3
  • 20
  • 30
  • Though I’d want to further add syntax for Swift 2.3: guard let label = sender.view as? UILabel else { print("Error not a label"); return } let position = sender.locationInView(self.tableView) guard let index = self.tableView.indexPathForRowAtPoint(position) else { print("Error label not in tableView"); return } self.tableView.beginUpdates() defer { self.tableView.endUpdates() } self.data[index.row] = “Changed Value" self.tableView.reloadRowsAtIndexPaths([index], withRowAnimation: .Automatic) – waseefakhtar Jan 27 '17 at 21:51
  • 1
    I have added your Swift 2.3 version :-) – Emptyless Jan 27 '17 at 22:58
0

Cells are reusable. That means when you change a value on a cell, it will be changed and stay changed on all reuses of that specific cell identifier, until you change it back.

To solve your issue, you can either set a "placeholder"/DefaultText text in cellForRowAtIndexPath , or if you have subclassed your cell, you can set the defaultText/placeholder in prepareForReuse method.

  • I'm not too sure about "it will be changed and stay changed on all reuses".. If your data source has changed, and you scroll a table view cell out of view, for example, when you come back, `cellForRow` is called again and more than likely your cell configuration code is in that method, so the cell's subviews will be set with whatever data you give it. – Stephen Paul Jan 27 '17 at 20:17
  • @StephenPaul He is changing the text directly, if you look at his code, he is not updating any datasource and the updated text is not referenced in his datasource. Also, he is not setting and value to the label.text from any datasource, he probably is using a static "placeholder". If you are changing a value of a cell property once, and you have no method at all calling it again, or changing it in your cellForRow method or prepareForReuse, your cell will end up with the changed value. –  Jan 27 '17 at 21:03
0

You can try this approach:

func labelTapped(recognizer: UITapGestureRecognizer){
    let dataCell: DataTableViewCell =  recognizer.view as! DataTableViewCell
    let indexPath = tableView.indexPath(for: dataCell)
    data[(indexPath?.row)!] = "changed value "
    self.tableView.reloadData()
}
Mat
  • 6,236
  • 9
  • 42
  • 55
  • As pointed out in the comments this will not work with reusable cells. As soon as the cell is reused the cell will take back the original DataSource value. – Emptyless Jan 27 '17 at 20:34
  • it works for me in a calendar which has multiple reusable cells. it's 2 lines of code, I think it worths trying. ;) – Mat Jan 27 '17 at 20:36
  • Here is an example where the label is modified directly: http://imgur.com/a/G9Jjz, maybe your cells never leave the screen - and thus never get reused? – Emptyless Jan 27 '17 at 20:41
  • I got your point but that's a problem of persistent storage and how you update the data. The problem he was facing is update one label only instead of change the text of the other cells. In my calendar I update core data and then `reloadData()` using the `NSFetchedResultsController` delegate method `controllerDidChangeContent` – Mat Jan 27 '17 at 20:46
  • The point i'm making is even if you update that one label it is never stored so any re-render of the screen will revert to the previous DataSource version. Therefor it is not sufficient / not a viable solution to only change the text property directly but rather change the datasource and notify the TableView with either ResultsSearchController or directly with tableViewBeginUpdates and reloading cells on certain indexes – Emptyless Jan 27 '17 at 20:49
  • This won’t work. It’s applying the “Test” text to every other label in other cells as I scroll down. Could you be more clear with the `NSFetchedResultsController` delegate method `controllerDidChangeContent`? – waseefakhtar Jan 27 '17 at 21:27
  • @waseefakhtar you are right... that wasn't working. If you apply the gesture recognizer to the cell instead of the label the above code works, Emptyless approach works applying that to the label. He is also right about the reusable cell but I think you want to store the "updated data" in some way (like coreData). right? – Mat Jan 27 '17 at 21:33
  • I don't apply the text to the UILabel ;) I alter the DataSource where the UITableCell get's its data from and then reload the UITableView at the index of the cell that is altered. – Emptyless Jan 27 '17 at 23:03
  • 1
    @Emptyless I meant the gesture recognizer – Mat Jan 27 '17 at 23:27