How to add gesture to UITableViewCell? - ios

I want to add a tap gesture to every cell in a UITableView that edits the content in it. The two ways to add a gesture are in code or through storyboard. I tried both and they failed.
Can I add a gesture to every cell in table with storyboard drag and drop? It seems to only add gesture to the first cell. Adding gesture in code, I wrote something like,
addGestureRecognizer(UITapGestureRecognizer(target: self,action:#selector(MyTableViewCell.tapEdit(_:))))
or
addGestureRecognizer(UITapGestureRecognizer(target: self, action:"tapEdit:"))
both work. But I'd like to let the UITableViewController handle this gesture because it does something with the datasource. How do I write my target and action?
EDIT:
addGestureRecognizer(UITapGestureRecognizer(target: MasterTableViewController.self, action:#selector(MasterTableViewController.newTapEdit(_:)))
it induce an error said, unrecognized selector sent to class 0x106e674e0...

To add gesture to UITableViewCell, you can follow the steps below:
First, add gesture recognizer to UITableView
tapGesture = UITapGestureRecognizer(target: self, action: #selector(tableViewController.tapEdit(_:)))
tableView.addGestureRecognizer(tapGesture!)
tapGesture!.delegate = self
Then, define the selector. Use recognizer.locationInView to locate the cell you tap in tableView. And you can access the data in your dataSource by tapIndexPath, which is the indexPath of the cell the user tapped.
func tapEdit(recognizer: UITapGestureRecognizer) {
if recognizer.state == UIGestureRecognizerState.Ended {
let tapLocation = recognizer.locationInView(self.tableView)
if let tapIndexPath = self.tableView.indexPathForRowAtPoint(tapLocation) {
if let tappedCell = self.tableView.cellForRowAtIndexPath(tapIndexPath) as? MyTableViewCell {
//do what you want to cell here
}
}
}
}
It is possible to add gesture directly to TableView cell and access the datasource in viewController, You need to set up a delegate:
In your custom cell:
import UIKit
class MyTableViewCell: UITableViewCell {
var delegate: myTableDelegate?
override func awakeFromNib() {
super.awakeFromNib()
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(MyTableViewCell.tapEdit(_:)))
addGestureRecognizer(tapGesture)
//tapGesture.delegate = ViewController()
}
func tapEdit(sender: UITapGestureRecognizer) {
delegate?.myTableDelegate()
}
}
protocol myTableDelegate {
func myTableDelegate()
}
In your viewController:
import UIKit
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource, UIGestureRecognizerDelegate, myTableDelegate {
#IBOutlet var tableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
tableView.delegate = self
tableView.dataSource = self
// Do any additional setup after loading the view, typically from a nib.
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 35
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as? MyTableViewCell
cell?.delegate = self
return cell!
}
func myTableDelegate() {
print("tapped")
//modify your datasource here
}
}
However, this method could cause problems, see UIGestureRecognizer and UITableViewCell issue. In this case, when the swipe gesture successes, the selector get called twice for some reason. I can't say the second method is a bad one as I haven't found any direct evidence yet, but after searching through Google, it seems like the first method is the standard way.

You don't need to add gesture recognizer to achieve what you are doing.
Use the UITableViewDelegate method tableView:didSelectRowAtIndexPath: to detect which row is tapped (this is what exactly your tapGesture is going to do) and then do your desired processing.
If you don't like the gray indication when you select cell, type this in your tableView:didEndDisplayingCell:forRowAtIndexPath: just before returning the cell:
cell?.selectionStyle = .None

Adding gesture in awakeFromNib method seems much more easier and works fine.
class TestCell: UITableViewCell {
override func awakeFromNib() {
super.awakeFromNib()
let panGesture = UIPanGestureRecognizer(target: self,
action: #selector(gestureAction))
addGestureRecognizer(panGesture)
}
#objc func gestureAction() {
print("gesture action")
}
}

The easiest way to do this is to add the gesture in a custom UITableViewCell. An easier alternative to setting up a custom delegate pattern is to inform the view controller of the edits would be to use a handler in the form of a closure that the view controller can provide and which is called when user editing is finished. I'm assuming a textField is used to allow cell editing.
class CustomTableViewCell: UITableViewCell {
func activateTitleEditing() {
textField.isEnabled = true
textField.becomeFirstResponder()
}
// This will hold the handler closure which the view controller provides
var resignationHandler: (() -> Void)?
#objc private func tap(_ recognizer: UITapGestureRecognizer) {
guard recognizer.state == .ended else { return }
activateTitleEditing()
}
#IBOutlet weak var textField: UITextField! { didSet {
textField.delegate = self
let tap = UITapGestureRecognizer(target: self, action: #selector(tap(_:)))
addGestureRecognizer(tap)
textField.isEnabled = false
}}
}
extension CustomTableViewCell: UITextFieldDelegate {
func textFieldDidEndEditing(_ textField: UITextField) {
resignationHandler?()
}
}
And within your custom UITableViewController, pass in the handler to be able to make changes to your model. Don't forget to account for possible memory cycles in the closure.
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// initialize and return table view cell
let cell = tableView.dequeueReusableCell(withIdentifier: K.documentCellIdentifier, for: indexPath)
assert(cell is CustomTableViewCell, "Document cell dequeuing error")
let customCell = cell as! DocumentTableViewCell
customCell.textField.text = documentModel.documents[indexPath.row]
customCell.resignationHandler = { [weak self, unowned customCell] in
guard let self = self else { return }
if let newTitle = customCell.textField.text {
self.cellModel.cells[indexPath.row] = newTitle
}
}
return customCell
}

Related

BEMCheckBox delegate is not called

In my iOS Switch app I have a BEMCheckBox in each table cell. When dequeuing a cell I want to set a delegate that gets called.
My problem is that the checkbox works fine but the delegate is not never called. How to add a delegate to each checkbox?
I want to know which indexPath for checkbox. The plan is to pass model object to the delegate and update it accordingly.
Table cell
let cell = tableView.dequeueReusableCell(withIdentifier: "MyCell", for: indexPath)
cell.doneCheckbox.delegate = DoneBEMCheckBoxDelegate()
return cell
Delegate is very simple
class DoneBEMCheckBoxDelegate: NSObject, BEMCheckBoxDelegate {
#objc func didTap(_ checkBox: BEMCheckBox) {
print("Checkbox tapped")
}
}
cell.doneCheckbox.delegate = DoneBEMCheckBoxDelegate() is creating a new DoneBEMCheckBoxDelegate object in a local variable and assigning that as the delegate. Since the delegate property is weak, it will be released as soon as the function exits because there is no strong reference remaining.
I would suggest that having a separate object class to be the delegate probably isn't what you want anyway.
I would set the cell to be the check box delegate and then declare another protocol so that the cell can have its own delegate, which would be your table view controller.
protocol MyCellDelegate {
func checkBox(for cell: MyCell, isOn: Bool)
}
class MyCell: UITableViewCell, DoneBEMCheckBoxDelegate {
var delegate: MyCellDelegate?
override func awakeFromNib() {
super.awakeFromNib()
self.doneCheckBox.delegate = self
}
#objc func didTap(_ checkBox: BEMCheckBox) {
print("Checkbox tapped")
self.delegate?.checkBox(for: self, isOn: checkBox.isOn)
}
}
class YourViewController: MyCellDelegate {
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
...
cell.delegate = self
return cell
}
func checkBox(for cell: MyCell, isOn: Bool) {
guard let indexPath = tableView.indexPath(for: cell) else {
return
}
// Now do whatever you need to with indexPath
}
}
This way you avoid creating additional objects and datastructures and you won't have a problem if cells are re-ordered as there is no dependency on index path.
I noticed that delegate is a weak reference in checkbox class, as it is supposed to be :) So my delegate was freed after method scope ended.
I fixed this by storing delegates in view controller during their usage.
var checkboxDelegates: [IndexPath:DoneBEMCheckBoxDelegate] = [:]
...
let checkboxDelegate = DoneBEMCheckBoxDelegate(realm: realm, set: set)
checkboxDelegates[indexPath] = checkboxDelegate
cell.doneCheckbox.delegate = checkboxDelegate

What the best delegate or get superview?

Can anyone help me to give reason to use delegate/protocol oriented or get superview, as I know swift use protocol oriented on code but for grab parent view or controller we still can use get superview like
Get superview example:
extension UIView {
var parentViewController: UIViewController? {
var parentResponder: UIResponder? = self
while parentResponder != nil {
parentResponder = parentResponder!.next
if let viewController = parentResponder as? UIViewController {
return viewController
}
}
return nil
}
}
Use delegate example:
protocol SomeDelegate {
func didClick()
}
class Child {
var delegate: SomeDelegate?
}
What Pros and Cons to use delegate or get superview ?
Example for parentView:
class Cell {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
self.parentViewController.view.makeToast("Something !")
}
}
Example for delegate:
class Parent: SomeDelegate {
func didClick() {
self.view.makeToast("Something !")
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableview.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as? Cell
cell.delegate = self
return cell
}
}
class Cell {
var label: UILabel
var delegate: SomeDelegate?
func configure() {
label.addGestureRecognizer(UILongPressGestureRecognizer(
target: self,
action: #selector(copyAction(_:))
))
}
#objc private func copyAction(_ sender: UILongPressGestureRecognizer) {
guard let delegate = self.delegate else {
return
}
delegate.didClick()
}
}
Delegate is preferred, not the superview. some reasons below
A view added in stack is not always retained in memory after its addition. especially when no strong reference maintained (this differs when view added from XIB or SB). So in this case calling superview might sometime crash with an unrecognized selector sent on some random instance.
One can create a view and never add to another view. ex for sake of removing you might comment just addsubview line leaving other code as is. At this time also the superview is nil.
Usage of custom views under uicontrols with own reusable view stack like Collectionview,TableView etc. would change superviews in runtime. so not always guaranteed to call same superview instance.

How to present view controller from UILabel with tap gesture recognized in custom table view cell?

I have a custom dynamic table view cell with a label that has a tap gesture recognized added. When the user taps the label, not anywhere else in the cell, I want to present a view controller.
The instagram app has this feature. Ie. when you tap likes, it takes you to a likes table view, when you tap comments, it shows you to a comments table view. This is the same experience I want.
I am not looking to use didSelectRow because then it kind of defeats the purpose of having the specific target area to tap to show a new view controller.
So, how can I present a view controller from a tap gesture recognizer in a subclass of UITableViewCell?
UPDATED:
I am passing a closure to my custom TableViewCell which is successfully being called when the button is pressed. But I am stuck in the TableView and cannot pass information to the next View Controller I want to present. And I can't actaully perform the segue either :\
// From UITableView
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let story = stories[indexPath.row]
if let cell = tableView.dequeueReusableCell(withIdentifier: "Fun Cell", for: indexPath) as? FunTableViewCell {
cell.configureCell(title: story.title, info: story.info)
cell.buttonAction = { [weak self] (cell) in
print("the button was pressed for \(story.title)")
self?.buttonWAsTapped(title: story.title)
}
return cell
} else {
return FunTableViewCell()
}
}
func buttonWAsTapped(title: String) {
// Need to pass something to the next View Controller... but how???
if let nextVC = UIViewController() as? DetailViewController {
nextVC.storyTitle = title
performSegue(withIdentifier: "Button Pressed", sender: self)
}
}
// Custom TableViewCell
class FunTableViewCell: UITableViewCell {
#IBOutlet weak var funLabel: FunLabel!
#IBOutlet weak var standardLabel: TappedLabel!
#IBOutlet weak var funButton: FunButton!
var buttonAction: ((UITableViewCell) -> Void)?
override func awakeFromNib() {
super.awakeFromNib()
let tap = UITapGestureRecognizer(target: self, action: #selector(readMoreTapped))
tap.numberOfTapsRequired = 1
standardLabel.addGestureRecognizer(tap)
standardLabel.isUserInteractionEnabled = true
}
#IBAction func btnPressed(sender: UIButton) {
print("Button pressed")
buttonAction?(self)
}
When creating the cell, pass a block to it. That's the handler for the button.
When the button tapped, call the block.
You can save the block as a property of the subclass of UITableViewCell.
In your tableViewCell class, add a property:
class CustomTableViewCell: UITableViewCell {
open var completionHandler: (()->Void)?
}
In your viewController that has the tableView:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = CustomTableViewCell()
cell.completionHandler = {
() -> Void in
let newViewController = UIViewController()
//configure the VC here base on the indexPath
self.present(newViewController, animated: true, completion: nil)
}
return cell
}

UIButton inside table not triggering

Hello I'm trying to figure out how to call a UIButton inside a custom cell within a UItable in storyboard. At the moment I have a library that creates a sidemenu working just fine (more info here) and I can see the button I placed when I launch the simulator. However, when I click on the button the action is not triggered, can you please guide me as to how I can achieve this?
Important to note that the table was create entirely in storyboard.
My work in progress code within TopratedVC.swift to get the button to trigger the action:
override func viewDidLoad() {
super.viewDidLoad()
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("UITableViewVibrantCell") as! CellClassMenu
cell.sendFeedBackBtn.tag = indexPath.row
cell.sendFeedBackBtn.addTarget(self, action: "sendFeedBackBtnAction:", forControlEvents: .TouchUpInside)
cell.contentView.userInteractionEnabled = false //tried with true as well, no difference
cell.bringSubviewToFront(cell.sendFeedBackBtn)
cell.userInteractionEnabled = true
return cell
}
func sendFeedBackBtnAction(sender: UIButton){
print("sendFeedBackBtnAction tapped")
}
My UITableViewVibrantCell.swift file contains the following:
import UIKit
class UITableViewVibrantCell: UITableViewCell {
#IBOutlet var sendFeedBackBtn: UIButton!
}
My sndFeedBackBtn has a referencing outlet to UITableViewVibrantCellsendFeedBackBtn which has a class of UITableViewVibrantCell. What am I doing wrong? Thank you.
What it looks like in simulator:
In your post, you show a UITableViewVibrantCell class, and dequeue a cell with the "UITableViewVibrantCell" identifier, but cast it as CellClassMenu?
Anyhow, it would be better practice to create a cell delegate for actions, and let your controller decide the implementation, rather than adding a target every time the cell is dequeued. You can do that like so:
UITableViewVibrantCell
import UIKit
protocol UITableViewVibrantCellDelegate: NSObjectProtocol {
func buttonPressed(sender: UIButton)
}
class UITableViewVibrantCell: UITableViewCell {
var delegate: UITableViewVibrantCellDelegate?
#IBOutlet var feedbackButton: UIButton!
override func awakeFromNib() {
super.awakeFromNib()
feedBackButton.addTarget(self, action: #selector(self.buttonPressed(_:)), forControlEvents: .TouchUpInside)
}
func buttonPressed(sender: UIButton) {
delegate?.buttonPressed(sender)
}
}
TopratedVC
class TopratedVC: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("UITableViewVibrantCell") as! UITableViewVibrantCell
cell.delegate = self
return cell
}
// MARK: - UITableViewVibrantCellDelegate
func buttonPressed(sender: UIButton) {
print("feedbackButton tapped")
}
}
Selectors ("sendFeedBackBtnAction:") can't pass parameters. And a param isn't needed in the sendFeedBackBtnAction function since you're calling it only for this button. So change I'd change it to simply...
func sendFeedBackBtnAction()
then I'd also recommend changing your selector to a more updated swift version...
cell.sendFeedBackBtn.addTarget(self, action: #selector(sendFeedBackBtnAction), forControlEvents: .TouchUpInside)

How to pass an object associated with UITapGestureRecognizer

I have a CustomTableViewCell where there is an image. This image is associated with UITapGestureRecognizer.
When i click in this image i need pass the object, based on the cell and indexPath, to the selector, like this:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("cellTotal") as TotalTableViewCell
let schedulle = schedulleList[indexPath.row] as Schedulle
let tap = UITapGestureRecognizer(target: self, action: "edit:")
tap.numberOfTapsRequired = 1
tap.numberOfTouchesRequired = 1
//simulation code - here i need pass the object clicked
tap.AssociatedObject = schedulle
cell.imgSchedulleEdit.addGestureRecognizer(tap)
return cell
}
func edit(sender: UITapGestureRecognizer) {
//simulation code getAssociated Object
let schedulle = sender.getAssociatedObject as Schedulle
}
I know, this is not good, generally i use didSelectRow, but this app needs a click on specific image of cell.
Schedulle have attributes:
import Foundation
import CoreData
class Schedulle: NSManagedObject {
#NSManaged var createdDate: NSDate
#NSManaged var message: String
}
Instead of adding the gesture recognizer in the dequeue cell method, add it in the TotalTableViewCell subclass with the cell as the target, then provide a delegate callback to your view controller, along the lines of totalTableViewCellDidTapImage(cell: TotalTableViewCell)
Now in your view controller you can get the index path for this row by using
func totalTableViewCellDidTapImage(cell: TotalTableViewCell) {
let indexPath = myTableView.indexPathForCell(cell)
let schedulle = schedulleList[indexPath.row] as Schedulle
// do stuff with schedulle
}
Instead of using a UIImageView, use UIButton, and use the button's method setImage(_:forState:) to set your image. Then you can you activate the event using addTarget, which will pass an instance of the button to the action.
According #someGuy and #backsquare, i'll post the complete answer:
I changed for button instead of button.
1 - I defined the protocol, in custom table view cell:
protocol SchedulleEdit : NSObjectProtocol{
func totalTableViewCellEdit(cell: TotalTableViewCell)
}
2 - i created the var, in custom table view cell to receive the delegate
protocol SchedulleEdit : NSObjectProtocol{
func totalTableViewCellEdit(cell: TotalTableViewCell)
}
class TotalTableViewCell: UITableViewCell {
var delegate : SchedulleEdit? = nil
...
}
3 - i defined the function to capture selected cell and send it to the view controller that implement our protocol.
protocol SchedulleEdit : NSObjectProtocol{
func totalTableViewCellEdit(cell: TotalTableViewCell)
}
class TotalTableViewCell: UITableViewCell {
var delegate : SchedulleEdit? = nil
...
}
#IBAction func edit(sender: UIButton) {
self.delegate?.totalTableViewCellEdit(self)
}
}
4 - In ViewController or in TableViewController, depends your case, i implemented the protocol SchedulleEdit:
class TotalViewController: UIViewController,UITableViewDataSource,UITableViewDelegate, SchedulleEdit{
...
}
5 - In cellForRowAtIndexPath i set the delegate for:
class TotalViewController: UIViewController,UITableViewDataSource,UITableViewDelegate, SchedulleEdit{
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("cellTotal") as TotalTableViewCell
cell.delegate = self
...
}
}
6 - Finally i implemented the required method of our protocol:
class TotalViewController: UIViewController,UITableViewDataSource,UITableViewDelegate, SchedulleEdit{
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("cellTotal") as TotalTableViewCell
cell.delegate = self
...
}
func totalTableViewCellEdit(cell: TotalTableViewCell) {
let indexPath = self.tblTotal.indexPathForCell(cell)
let row = indexPath?.row
let schedulle = self.schedulleList[row!] as Schedulle
println(schedulle.message)
}
}

Resources