Gesture recognizer in iOS UiTableViewCell's sub view - ios

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.

Related

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.

Does tap gestures added to cell gets preserved on reuse?

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)
}

How to convert number in UITextField and pass it to UILabel in UITableViewCell?

First, I want to build my app like this money converter.
Cell for Row 0 will have a UITextLabel as a subview of UITableViewCell, which gets a number from users.
Cell for Row 2 will have a UILabel as a subview of UITableViewCell, which will shows the calculated number.
Cell for Row 3 will have a UILabel as a subview of UITableViewCell, which will shows the calculated number. But this number would be different from the number at Row 2.
So I made 3 classes, for 'UITextField', 'UILabel', 'UITableViewController'
Here is the 'UITableViewController' class code.
class TableViewController: UITableViewController, UITextFieldDelegate {
let fruitsComponents: [String] = ["Apple", "Banana", "Grape", "Pear"]
let cellReuseidentifier = "cell"
let anotherCellReuseidentifier = "anotherCell"
override func viewDidLoad() {
super.viewDidLoad()
tableView.register(FruitTableViewCell.self, forCellReuseIdentifier: cellReuseidentifier)
tableView.register(AnotherFruitTableViewCell.self, forCellReuseIdentifier: anotherCellReuseidentifier)
}
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return fruitsComponents.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if indexPath.row == 0 {
let cell = tableView.dequeueReusableCell(withIdentifier: cellReuseidentifier, for: indexPath) as! FruitTableViewCell
cell.textLabel?.text = fruitsComponents[indexPath.row]
return cell
} else {
let cell = tableView.dequeueReusableCell(withIdentifier: anotherCellReuseidentifier, for: indexPath) as! AnotherFruitTableViewCell
cell.textLabel?.text = fruitsComponents[indexPath.row]
return cell
}
}
}
I can get a number from UITextField and calculate it in UITextLabel subclass file.
Or, I can do this in cellForRow syntex in UITableViewController class by making UITextLabel instance directly.
But the problem is, I cannot pass the calculated data to UILabel. Because, TableViewCell was made by 'dequeue' method.
I cannot pass the calculated data because the cells which contains UILabel subview were not made.
Even if the cells are made, I cannot pass the data in 'cell' to 'cell', because both 'cell' and 'cell' was made by 'dequeue' method using same 'let cell'.
Of course, I cannot differentiate the cell at Row2 and the cell at Row 3.
Because of this, now, I cannot make a function that converts UILabel layout after receiving the data from UITextField.
How to convert number in UITextField and pass it to UILabel in UITextViewCell?
-------- Updates ---------
'FruitsTableViewCell' subclass code
class FruitTableViewCell: UITableViewCell, UITextFieldDelegate {
var numberTextField = UITextField()
let toolBarKeyBoard = UIToolbar()
let flexibleSpace = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil)
lazy var doneButton = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(donePressed))
var result : String!
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
numberTextField.addTarget(self, action: #selector(valueChanged(_:)), for: .valueChanged)
self.contentView.addSubview(numberTextField)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
numberTextField.frame = CGRect(x: 250, y: 7.5, width: 100, height: 30)
numberTextField.keyboardType = .numberPad
toolBarKeyBoard.sizeToFit()
numberTextField.inputAccessoryView = toolBarKeyBoard
toolBarKeyBoard.setItems([flexibleSpace, doneButton], animated: false)
}
#objc func valueChanged(_ textField: UITextField) {
var dataDict: [String: Double] = [:]
dataDict["amount"] = Double(numberTextField.text!) ?? 0
NotificationCenter.default.post(name: Notification.Name(rawValue: "AmountChanged"), object: nil, userInfo: dataDict)
}
#objc func donePressed() {
numberTextField.resignFirstResponder()
}
}
'AnotherFruitTableViewCell' subclass code
class AnotherFruitTableViewCell: UITableViewCell {
var outputTextLabel = UILabel()
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
NotificationCenter.default.addObserver(self, selector: #selector(handleNewAmount(_:)), name: Notification.Name("AmountChanged"), object: nil)
self.contentView.addSubview(outputTextLabel)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
outputTextLabel.frame = CGRect(x: 250.0, y: 7.5, width: 100.0, height: 30.0)
}
#objc func handleNewAmount(_ notification: Notification) {
guard let userInfo = notification.userInfo, let amount = userInfo["amount"] as? Double else {
return
}
let finalAmount = amount * 0.05
outputTextLabel.text = String(finalAmount)
}
}
This is a perfect example for using Notifications. You should add observers to every cell which contains the label. I'm guessing AnotherFruitTableViewCell is the cell which contains the label and FruitTableViewCell is the one that contains text field.
class AnotherFruitTableViewCell: UITableViewCell {
// Initialization of label, country and other code
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
NotificationCenter.default.addObserver(self, selector: #selector(handleNewAmount(_:)), name: Notification.Name("AmountChanged"), object: nil)
}
#objc func handleNewAmount(_ notification: Notification) {
guard let userInfo = notification.userInfo, let amount = userInfo["amount"] as? Float else {
return
}
let finalAmount = //Some formula
//Make the necessary changes to amount based on the country
label.text = finalAmount
}
}
Note: If you need to calculate the amount for different countries, you
would need something in the label cell to identify the country and
handle that when you set the amount to the label from the
notification.
Now all you need to do is post a Notification from the text field cell when the value of the text field changes.
class FruitTableViewCell: UITableViewCell, UITextFieldDelegate {
// Initialization of text field and other code
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
textField.addTarget(self, action: #selector(valueChanged(_:)), for: .editingChanged)
}
#objc func valueChanged(_ textField: UITextField) {
var amount: [String: Float] = [:]
amountDict["amount"] = Int(textField.text!) ?? 0
NotificationCenter.default.post(name: Notification.Name(rawValue: "AmountChanged"), object: nil, userInfo: amountDict)
}
}
Edit: For done button click. Remove the target and implement the same thing in your button selector.
#objc func donePressed() {
numberTextField.resignFirstResponder()
var amount: [String: Float] = [:]
amountDict["amount"] = Int(numberTextField.text!) ?? 0
NotificationCenter.default.post(name: Notification.Name(rawValue: "AmountChanged"), object: nil, userInfo: amountDict)
}
Introduce model class(es) to your app, or at the very least some common "model" properties within your view controller (I cringe at saying that, but it's a first step toward understanding and incorporating model data). Have the viewController code that handles each control reference these same model classes, including code to do the computations based on what are in the model classes.
To trigger an update to all the UI in the table, just call self.tableView.reloadData(). Reloading the visible cells will be plenty fast, and the labels will repopulate from the model data.
(Then someday graduate from MVC to MVVM and have your VM class access the models and do the computations, letting your VC simply blindly update the controls per what the VM gives it.)

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
}

Assigning property of custom UITableViewCell

I am using the below custom UITableViewCell without nib file.
I have no problem in viewing it in my UITableViewController.
My problem is with assigning a text to "name" label cell.name.text = "a Name" .. noting is assigned
Can you help me?
import UIKit
class ACMenuCell: UITableViewCell {
#IBOutlet var name : UILabel!
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
name = UILabel(frame: CGRectMake(20, 10, self.bounds.size.width , 25))
name.backgroundColor = .whiteColor()
self.contentView.addSubview(name)
self.contentView.backgroundColor = .blueColor()
}
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override func layoutSubviews() {
super.layoutSubviews()
}
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}
override func setSelected(selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
}
You might want to try this: Create a public function in a subclass of UITableViewCell, and inside this funxtion set the text to label.
(void)initializeCell
{
do whatever like setting the text for label which is a subview of tableViewCell
}
And from cellForRowAtIndexPAth, call this function on cell object:
// taking your code for demo
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { let reuseIdentifierAC:NSString = "ACMenuCell"; var cell = tableView.dequeueReusableCellWithIdentifier(reuseIdentifierAC, forIndexPath:indexPath) as ACMenuCell cell = ACMenuCell(style: UITableViewCellStyle.Default, reuseIdentifier: reuseIdentifierAC) [cell initializeCell] return cell }
This approach has worked for me.

Resources