Does tap gestures added to cell gets preserved on reuse? - ios

I have a cell with an image. I am adding tap gesture to it tableview delegate method. When the cell gets reused, does the tap gesture duplicated? What happens to the tap gesture?
class CalendarCell: UITableViewCell {
#IBOutlet weak var locationImageView: UIImageView!
}
class CalendarViewController: UIViewController {
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "calendarCell") as! CalendarCell
let locationLabelTap = UITapGestureRecognizer(target: self, action: #selector(locationDidTap(recognizer:)))
cell.locationLabel.addGestureRecognizer(locationLabelTap)
return cell
}
#objc func locationDidTap(recognizer: UITapGestureRecognizer) {
}
}

Short answer: Yes
Long answer:
You shouldn't be doing it like that. Add the tap gesture when the cell is initialized. This way the tap is added only once when it is created and not everytime it is reused.
class CalendarCell: UITableViewCell {
//Your variable declaration and other stuff
.
.
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
//Adding subviews, constraints and other stuff
.
.
.
let locationLabelTap = UITapGestureRecognizer(target: self, action: #selector(locationDidTap(recognizer:)))
locationLabel.addGestureRecognizer(locationLabelTap)
}
.
.
.
}
If you are using a storyboard, you should do the same in awakeFromNib file as pointed out by #DuncanC.
override func awakeFromNib() {
super.awakeFromNib()
.
.
.
let locationLabelTap = UITapGestureRecognizer(target: self, action: #selector(locationDidTap(recognizer:)))
locationLabel.addGestureRecognizer(locationLabelTap)
}

Related

Gesture recognizer in iOS UiTableViewCell's sub view

I tried to add gesture recognizer in UITableViewCell's sub view. That doesn't seem to work. If add one for the cell it's working. Any idea why?
#objc public class MyCell: UITableViewCell {
...
#objc private let subView = createSubView()
...
#objc public override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
isUserInteractionEnabled = true
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(tap(_:)))
subView.addGestureRecognizer(tapGesture)
subView.isUserInteractionEnabled = true
addSubview(subView)
}
#objc private func tap(_ sender: UITapGestureRecognizer) {
// never happen
return
}
The reason for this is to detect complex gesture inside a view so table view's default delegate or cell's gesture handler is not enough.

How to avoid adding more than one tap gesture to any cell?

I have a weird problem. When scrolling down, cells disappear if Tap Gesture happened.
Looks like I need to stop adding Tap Gesture to cells. I've done testing of this condition in function but it didn't work.
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! ToDoItemsCell
...
cell.textField.delegate = self
cell.textField.isHidden = true
cell.toDoItemLabel.isUserInteractionEnabled = true
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(toDoItemLabelTapped))
tapGesture.numberOfTapsRequired = 1
cell.addGestureRecognizer(tapGesture)
return cell
}
And here is my function:
#objc func toDoItemLabelTapped(_ gesture: UITapGestureRecognizer) {
if gesture.state == .ended {
let location = gesture.location(in: self.tableView)
if let indexPath = tableView.indexPathForRow(at: location) {
if let cell = self.tableView.cellForRow(at: indexPath) as? ToDoItemsCell {
cell.toDoItemLabel.isHidden = true
cell.textField.isHidden = false
cell.textField.becomeFirstResponder()
cell.textField.text = cell.toDoItemLabel.text
}
}
}
}
Tapping works, but it keeps adding to other cells and makes them disappear. What can be the issue?
Gesture should be added once to each cell. In your code gesture will be added every time cellForRowAt will be called and it will be called many times especially when you scroll down to list.
Move you gesture add code to ToDoItemsCell class and than you can use delegates to inform your view controller when cell gets tapped.
protocol ToDoItemsCellDelegate {
toDoItemsCellDidTapped(_ cell: ToDoItemsCell)
}
class ToDoItemsCell : UITableViewCell {
weak var delegate: ToDoItemsCellDelegate?
var indexPath: IndexPath!
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
// code common to all your cells goes here
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(toDoItemLabelTapped))
tapGesture.numberOfTapsRequired = 1
self.addGestureRecognizer(tapGesture)
}
#objc func toDoItemLabelTapped(_ gesture: UITapGestureRecognizer) {
delegate?.toDoItemsCellDidTapped(self)
}
}
In function cellForRowAt you can just select the delegate and set indexPath.
Note:
If you just wanted to perform action when user taps any cell you can use didSelectRowAt method of UITableViewDelegate.

Adding a gesture recognizer to an image view in a table cell

How can I add a Gesture Recognizer to a UIImageView in a table cell? I want it so that if a user taps an image in the cell, the image will change and the data model will update.
I know this needs to be set up in the UITableViewController. My code currently can execute a command if anywhere in the cell is tapped, but I would like it to execute only if the image is tapped, not anywhere in the cell.
I setup up the gesture recognizer in viewDidLoad
override func viewDidLoad() {
super.viewDidLoad()
// Load sample data
loadSampleHabits()
// Initialize tap gesture recognizer
var recognizer = UITapGestureRecognizer(target: self, action: #selector(tapEdit(recognizer:)))
// Add gesture recognizer to the view
self.tableView.addGestureRecognizer(recognizer)
And this is the function
//action method for gesture recognizer
func tapEdit(recognizer: UITapGestureRecognizer) {
if recognizer.state == UIGestureRecognizerState.ended {
let tapLocation = recognizer.location(in: self.tableView)
if let tapIndexPath = self.tableView.indexPathForRow(at: tapLocation) {
if let tappedCell = self.tableView.cellForRow(at: tapIndexPath) as? HabitTableViewCell {
print("Row Selected")
}
}
}
As a secondary question, are there any conflicts if I want to add a gesture recognizer to the cell and the image view within the cell?
You are adding gesture recognizer on your tableview instead of imageView as you required. Yo need to move your code from viewDidLoad to cellForRowAtIndexPath and add gesture to imageView in each cell while configuing your cell.
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell{
var recognizer = UITapGestureRecognizer(target: self, action: #selector(tapEdit(recognizer:)))
// Add gesture recognizer to your image view
cell.yourimageview.addGestureRecognizer(recognizer)
}
Note: Do make sure to enable userinteraction of your image view
cell.yourimageview.userInteractionEnabled = YES;
For your requirement I will suggest using UILongPressGestureRecognizer as it has less chances of conflict in gesture and didselect. Yo can add UILongPressGestureRecognizer in viewDidLoad and access it as per your requirement.
let lpgr = UILongPressGestureRecognizer(target: self, action: #selector(ViewController.handleLongPress(_:)))
lpgr.minimumPressDuration = 1
tableView.addGestureRecognizer(lpgr)
Define method as
func handleLongPress(_ gesture: UILongPressGestureRecognizer){
if gesture.state != .began { return }
let tapLocation = gesture.location(in: self.tableView)
if let tapIndexPath = self.tableView.indexPathForRow(at: tapLocation) {
if let tappedCell = self.tableView.cellForRow(at: tapIndexPath) as? HabitTableViewCell {
print("Row Selected")
}
}
You can try removing if recognizer.state == UIGestureRecognizerState.ended condition from your method.
UITapGestureRecognizer is a discrete gesture, and as such, your event handler is called only once when the gesture was recognized. You don't have to check the state at all. Certainly you won't receive a call for the state of .Began. For more info consider #Rob ans here.
Add This line in cell for row at index path
var recognizer = UITapGestureRecognizer(target: self, action: #selector(tapEdit(recognizer:)))
// Add gesture recognizer to the view
cell.yourimageviewname.addGestureRecognizer(recognizer)
cell.yourimageviewname.userInteractionEnabled = true;
For my suggestion you have to use UIButton in cell, for performance
improvements,
UIButtons
Specially designed for this and have been extensively optimized by Apple for touches.
If you want image in cell you can use UIButton with Image inside.
I have had design a solution like this. I just write a sample code below:
import UIKit
protocol CellImageTapDelegate {
func tableCell(didClickedImageOf tableCell: UITableViewCell)
}
class SampleCell : UITableViewCell {
var delegate : CellImageTapDelegate?
var tapGestureRecognizer = UITapGestureRecognizer()
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
initialize()
}
private func initialize() {
tapGestureRecognizer.addTarget(self, action: #selector(SampleCell.imageTapped(gestureRecgonizer:)))
self.addGestureRecognizer(tapGestureRecognizer)
}
func imageTapped(gestureRecgonizer: UITapGestureRecognizer) {
delegate?.tableCell(didClickedImageOf: self)
}
}
class ViewController: UITableViewController, CellImageTapDelegate {
// CellImageTapDelegate
func tableCell(didClickedImageOf tableCell: UITableViewCell) {
if let rowIndexPath = tableView.indexPath(for: tableCell) {
print("Row Selected of indexPath: \(rowIndexPath)")
}
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 10
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "SampleCellID", for: indexPath) as! SampleCell
cell.delegate = self
return cell
}
}
remember to do following in storyboard
1. enable user interaction of imageview
2. set class of tableviewcell
3. set reuse identifier of tableviewcell
// create an instance of UITapGestureRecognizer and tell it to run
// an action we'll call "handleTap:"
let tap = UITapGestureRecognizer(target: self, action: Selector("handleTap:"))
// we use our delegate
tap.delegate = self
// allow for user interaction
cell.imageViewName.userInteractionEnabled = true
// add tap as a gestureRecognizer to tapView
cell.imageViewName.addGestureRecognizer(tap)
import UIKit
class UserInfoCell: UITableViewCell{
#IBOutlet weak var imagePlaceholder: UIImageView!
}
class ViewController: UIViewController,UITableViewDelegate,UITableViewDataSource,UIImagePickerControllerDelegate,UINavigationControllerDelegate {
#IBOutlet weak var tableView: UITableView!
let imagePicker = UIImagePickerController()
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 1
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "UserInfoCell" ,for: indexPath ) as! UserInfoCell
let recognizer = UITapGestureRecognizer(target: self, action: #selector(self.openGallery))
cell.imagePlaceholder.addGestureRecognizer(recognizer)
recognizer.numberOfTapsRequired = 1
cell.imagePlaceholder.isUserInteractionEnabled = true
cell.name.text = "Akshay"
if let data = UserDefaults.standard.data(forKey: "savedImage") {
cell.imagePlaceholder.image = UIImage(data: data as Data)
}
return cell
}
#objc func openGallery(){
imagePicker.sourceType = .photoLibrary
present(imagePicker,animated: true, completion: nil)
}
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
let userimage = info[UIImagePickerController.InfoKey.originalImage] as! UIImage
let imageData = userimage.jpegData(compressionQuality: 1)!
UserDefaults.standard.setValue(imageData, forKey: "savedImage")
print("image found")
self.imagePicker.dismiss(animated: true, completion: nil)
self.tableView.reloadData()
}
override func viewDidLoad() {
super.viewDidLoad()
imagePicker.delegate = self
tableView.tableFooterView = UIView()
}
}
This code select image from gallery using Tapgesture of ImageView inside a TableViewCell

Finding the indexPath of a cell with a gesture recogniser in handler method

I have a pan gesture recogniser on a UITableViewCell which is attached to a method called didPan(sender: UIPanGestureRecognizer).
How can I use this method to determine which cell in a tableView this was activated from?
A good way to do this is to add the gesture recognizer in the UITableViewCell subclass and also have a delegate property in that class as well. So in your subclass:
protocol MyCustomCellDelegate {
func cell(cell: MyCustomCell, didPan sender: UIPanGestureRecognizer)
}
class MyCustomCell: UITableViewCell {
var delegate: MyCustomCellDelegate?
override func awakeFromNib() {
let gesture = UIPanGestureRecognizer(target: self, action: "panGestureFired:")
contentView.addGestureRecognizer(gesture)
}
func panGestureFired(sender: UIPanGestureRecognizer) {
delegate?.cell(self, didPan: sender)
}
}
Then in cellForRowAtIndexPath you just assign you view controller as the cells delegate.
You could add your pan gesture to the UITableViewCell in cellForRowAtIndexPath.
Then extract the optional UITableViewCell and look up the indexPath in the tableView.
Make sure you setup the UITableView as an IBOutlet so you can get to it in didPan:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("PanCell", forIndexPath: indexPath)
let panGesture = UIPanGestureRecognizer(target: self, action: "didPan:")
cell.addGestureRecognizer(panGesture)
return cell
}
func didPan(sender: UIPanGestureRecognizer) {
// Sender will be the UITableViewCell
guard let cell = sender.view as? UITableViewCell else {
return
}
let indexPathForPan = tableView.indexPathForCell(cell)
}

Tappable UITableViewCell with an also tappable button in it

I have a prototype cell that has some labels on it and a button (well, its actually an imageView, not a button):
I want to achieve this behavior:
Tap on the button executes certain code, say println("foo"), but doesn't perform the "show detail" segue
Tap on the rest of the cell performs a show detail segue
If requirement #1 wasn't necessary, I'd do this:
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
selectedPlace = places[indexPath.row]
self.performSegueWithIdentifier("ShowPlaceSegue", sender: self)
}
What is the recommended way to achieve this?
This is not like HTML DOM events? (z-index, etc)
I tried (in a very naif attempt) the following:
class PlaceTableViewCell: UITableViewCell {
#IBOutlet weak var favoritedImageView: UIImageView!
#IBOutlet weak var nameLabel: UILabel!
#IBOutlet weak var administrativeAreaLevel3: UILabel!
func configureCellWith(place: Place) {
nameLabel.text = place.name
administrativeAreaLevel3.text = place.administrativeAreaLevel3
favoritedImageView.addGestureRecognizer(UIGestureRecognizer(target: self, action:Selector("bookmarkTapped:")))
}
func bookmarkTapped(imageView: UIImageView) {
println("foo")
}
}
But no matter if I click the imageView or the rest of the cell, the "show detail" segue is performed and the "foo" isn't printed.
What do you think of putting a UIView, "v", inside the prototype cell that contains the labels and making "v" tappable? something like this:
If I do that, will the cell be grayed while tapped? I'd like to keep that...
Sorry, it was a stupid problem:
The "naif" way was indeed the way to go. Indeed it works like HTML DOM!...
But I changed this:
func configureCellWith(place: Place) {
nameLabel.text = place.name
administrativeAreaLevel3.text = place.administrativeAreaLevel3
favoritedImageView.addGestureRecognizer(UIGestureRecognizer(target: self, action:Selector("bookmarkTapped:")))
}
func bookmarkTapped(imageView: UIImageView) {
println("foo")
}
For this:
func configureCellWith(place: Place) {
nameLabel.text = place.name
administrativeAreaLevel3.text = place.administrativeAreaLevel3
let gestureRecognizer = UITapGestureRecognizer(target: self, action: Selector("bookmarkTapped:"))
gestureRecognizer.numberOfTapsRequired = 1
favoritedImageView.userInteractionEnabled = true
favoritedImageView.addGestureRecognizer(gestureRecognizer)
}
func bookmarkTapped(sender: UIImageView!) {
println("foo")
}
As you can see, I was using UIGestureRecognizer instead of UITapGestureRecognizer
EDIT:
So, the above is right, but now I think its better to have the action function in the class that contains the tableView, instead of having the action in the cell class itself.
So, i've moved the addGestureRecognizer to the cellForRowAtIndexPath method, ie:
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("PlacePrototype", forIndexPath: indexPath) as! PlaceTableViewCell
// Configure the cell
let place = places[indexPath.row]
cell.configureCellWith(place)
// HERE!
let gestureRecognizer = UITapGestureRecognizer(target: self, action:Selector("bookmarkTapped:"))
gestureRecognizer.numberOfTapsRequired = 1
cell.favoritedImageView.userInteractionEnabled = true
cell.favoritedImageView.addGestureRecognizer(gestureRecognizer)
return cell
}
And the action:
func bookmarkTapped(gestureRecognizer: UIGestureRecognizer) {
// println("foo")
var point = gestureRecognizer.locationInView(self.tableView)
if let indexPath = self.tableView.indexPathForRowAtPoint(point)
{
places[indexPath.row].toggleBookmarked()
self.tableView.reloadData()
}
}
Assuming your tableView can be displayed five cells, then the cellForRow will to be called five times, and you will add an UITapGestureRecognizer to five imageView of different. but when you scrolling to the seventh cell, you will got a reused cell(maybe the first cell) in the cellForRow, the imageView of the cell had an UITapGestureRecognizer, if you add UITapGestureRecognizer to the imageView again will cause you tap once trigger multiple times.
You can try this:
class PlaceTableViewCell: UITableViewCell {
#IBOutlet weak var favoritedImageView: UIImageView!
var favoritedTappedBlock: ((Void) -> Void)? // block as callback
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
commonInit()
}
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
private func commonInit() {
let gestureRecognizer = UITapGestureRecognizer(target: self, action: Selector("favoritedImageViewTapped"))
gestureRecognizer.numberOfTapsRequired = 1
favoritedImageView.addGestureRecognizer(gestureRecognizer)
favoritedImageView.userInteractionEnabled = true
}
private func favoritedImageViewTapped() {
if let favoritedTappedBlock = self.favoritedTappedBlock {
favoritedTappedBlock()
}
}
}
And cellForRow:
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("PlacePrototype", forIndexPath: indexPath) as! PlaceTableViewCell
// Configure the cell
let place = places[indexPath.row]
cell.configureCellWith(place)
cell.favoritedTappedBlock = {
println("tapped")
}
return cell
}

Resources