Issue with UILongPressGestureRecognizer while using it in table view cell - ios

I am implementing a long press in the uitableview through storyboard in swift3. I have only one prototype cell set in the storyboard. But the problem is the long press is being detected only in the first cell. Rest of the cells are not listening to the long press gesture.
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 10
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
let row = indexPath.row
cell.textLabel?.text = "Label"
return cell
}
#IBAction func longPress(_ guesture: UILongPressGestureRecognizer) {
if guesture.state == UIGestureRecognizerState.began {
print("Long Press")
}
}
The warning shown in the console is:
at a time, this was never allowed, and is now enforced. Beginning with iOS 9.0 it will be put in the first view it is loaded into.

Attach the gesture to the tableview, and when the gesture is triggered figure out which indexPath was selected.
override func viewDidLoad() {
super.viewDidLoad()
let longPressRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(self.longPress(_:)))
tableView?.addGestureRecognizer(longPressRecognizer)
}
func longPress(_ guesture: UILongPressGestureRecognizer) {
if guesture.state == UIGestureRecognizerState.began {
let point = guesture.location(in: tableView)
let indexPath = tableView.indexPathForRow(at: point);
print("Long Press \(String(describing: indexPath))")
}
}
Because a tableview is a kind of scrollview it is best to attach the gestures to the tableview itself and not any of its subview. This way it is less likely to interfere with the other gestures that must be tracked.

You need to add gesture for all cell in cellForRowAtIndexPath
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
let row = indexPath.row
cell.textLabel?.text = "Label"
let longPressRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(HomeViewController.longPress(_:)))
cell?.addGestureRecognizer(longPressRecognizer)
return cell
}
func longPress(_ guesture: UILongPressGestureRecognizer) {
if guesture.state == UIGestureRecognizerState.began {
print("Long Press")
}
}

Related

UITableview Update Single Cell

I got a play button on my tableview custom cell, whenever I tapped the play button. I want the selected button image to change to pause image.
The issue is that all the other buttons images are getting updated.
So all the images are changing to the pause image, instead of the selected button.
I tried to get the indexpath of the button tapped and reload only that row, but that doesn't seem to make a difference.
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let playerCell = tableView.dequeueReusableCell(withIdentifier: "playerCell", for: indexPath) as! PlayerCell
let item = subcategory?.items?[indexPath.row]
// Tap gestures extension for cell button action
playerCell.playPause.addTapGestureRecognizer {
AudioController.shared.setupPlayer(item: item)
if let selectedCell = tableView.cellForRow(at: indexPath) as? PlayerCell {
selectedCell.playPause.setImage(#imageLiteral(resourceName: "pause"), for: .normal)
tableView.reloadRows(at: [IndexPath(row: indexPath.row, section: 1)], with: .none)
}
print("index \(indexPath)")
}
What you can do is add a tag to the button. So within the method that you create the cells override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell, you add a tag to the button within that cells that represents the indexPath. Then from within the selector that you assign the button, you can get the cell that you want to alter.
For example:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "yourCell", for: indexPath)
cell.button.tag = indePath.row
cell.button.addTarget(self, action: #selector(yourSelector(_sender:)), for: .touchUpInside)
}
func yourSelector(_sender: UIButton){
let cell = tableView.cellForRow(at: IndexPath(row: sender.tag, section: 0)) as! YourCellType
// Change the image, play/pause audio for that cell
}
When you tap on the button in cell, tableView(_:didSelectRowAt:) won't be trigger, so I suggest using delegate to detect button action.
And you need to keep tracking the change of the cell's button status.
For example:
PlayCell.swift
protocol PlayCellDelegate: class {
func playCellPlayButtonDidPress(at indexPath: IndexPath)
}
class PlayerCell: UITableViewCell {
let playButton: UIButton = {
let button = UIButton()
button.addTarget(self, action: #selector(playButtonPressed(_:)), for: .touchUpInside)
return button
}()
weak var delegate: PlayCellDelegate?
var item: MyItem? {
didSet {
if item?.status == .paused {
// set pause image for playButton here
} else if item?.status == .playing {
// set play image for playButton here
}
}
}
var indexPath: IndexPath?
#objc func playButtonPressed(_ sender: UIButton) {
guard let indexPath = self.indexPath else { return }
delegate?.playCellPlayButtonDidPress(at: indexPath)
}
}
Model.swift
struct Subcategory {
// ...
var items: [MyItem]?
}
struct MyItem {
// ...
var status: Status.stop
enum Status {
case playing, paused, stopped // etc..
}
}
TableViewController.swift
class TableViewController: UITableViewController, PlayCellDelegate {
private var subcategory: Subcategory?
private let cellId = "Cell"
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if let cell = tableView.dequeueReusableCell(withIdentifier: cellId, for: indexPath) as? PlayerCell {
cell.delegate = self
cell.item = subcategory?.items?[indexPath.row]
cell.indexPath = indexPath
return cell
}
return UITableViewCell()
}
func playCellPlayButtonDidPress(at indexPath: IndexPath) {
// you only need to change model here, and reloadRows will update the cell.
if subcategory?.items?[indexPath.row].status == .play {
subcategory?.items?[indexPath.row].status = .pause
} // other logic..
tableView.reloadRows(at: [indexPath], with: .none)
}
}
Hope it helps!

UITableView How to call a function after reordering cells

In my project I wanted to implement moving rows as well as deleting them but not with stock "Delete" button but by tapping the image that is within my custom UITableViewCell called QueueCell. I delete rows in function deleteByTap2 which uses the sender.tag (which is the cell.indexPath.row) to recognise which cell should be removed. Both moving and deleting work great on their own but when you move, for example, 6th row to 2nd it still carries the tag = 6 and because of that when I tap on image to delete the row, incorrect row gets deleted. I created a function reTag which is supposed to update tags of all cells within the sections and it works great after being called in deleteByTap2 function but when called at the end of
func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath)
it seems to not know the state of tableView after moving row. I searched the forum and I found that there was undocumented UITableViewDelegate function
- (void)tableView:(UITableView *)tableView didEndReorderingRowAtIndexPath:(NSIndexPath *)indexPath;
but I tried calling it and it seems it was removed (or maybe name changed)
When should I call the reTag function so it would work properly? So it would know the order of tableView after reordering it?
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! QueueCell
let item = sungs[indexPath.section].songsIn[indexPath.row]
cell.setup(item: item)
if indexPath.section == 2{
let tap = UITapGestureRecognizer(target: self, action: #selector(deleteByTap2(_:)))
tap.numberOfTapsRequired = 1
tap.numberOfTouchesRequired = 1
cell.artwork.addGestureRecognizer(tap)
cell.artwork.isUserInteractionEnabled = true
cell.artwork.tag = indexPath.row
}
return cell
}
override func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
var toMove: MPMediaItem
tableView.beginUpdates()
if sourceIndexPath.section == 2{
if player.isShuffle{
toMove = player.shufQueue[player.shufIndex + sourceIndexPath.row + 1]
player.shufQueue.remove(at: player.shufIndex + sourceIndexPath.row + 1)
if destinationIndexPath.section == 2{
player.shufQueue.insert(toMove, at: player.shufIndex + destinationIndexPath.row + 1)
}
}else{
toMove = player.defQueue[player.defIndex + sourceIndexPath.row + 1]
player.defQueue.remove(at: player.defIndex + sourceIndexPath.row + 1)
if destinationIndexPath.section == 2{
player.defQueue.insert(toMove, at: player.defIndex + destinationIndexPath.row + 1)
}
}
}
tableView.endUpdates()
reTag(section: destinationIndexPath.section)
}
//the beginUpdates()-endUpdates() doesn't do much good here, actually it messes some of my cells
func reTag(section: Int){
var indexPath: IndexPath
for row in 0 ..< tableView.numberOfRows(inSection: section){
indexPath = IndexPath(row: row, section: section)
if let cell = tableView.cellForRow(at: indexPath) as? QueueCell{
cell.artwork.tag = row
}
}
}
func deleteByTap2(_ sender: UITapGestureRecognizer){
let tag = (sender.view?.tag)!
if player.isUsrQueue{
player.usrQueue.remove(at: player.usrIndex + tag + 1)
player.usrQueueCount! -= 1
sungs[2].songsIn.remove(at: tag)
}else{
player.defQueue.remove(at: player.defIndex + tag + 1)
sungs[2].songsIn.remove(at: tag)
player.defQueueCount! -= 1
}
let indexPath = IndexPath(row: tag, section: 2)
tableView.deleteRows(at: [indexPath], with: .fade)
reTag(section: 2)
}
This is a good example of why it's a bad idea to use .tag on objects to try and track them in this way.
I'd suggest that you move the tap gesture inside your cell class, and add a "call back" closure. This sample is, of course, missing your data class and cell.setup() code, but you should be able to see what needs to be changed:
// cell class
class QueueCell: UITableViewCell {
#IBOutlet weak var artwork: UIImageView!
var tapCallback: ((QueueCell) -> ())?
func addTap() {
if artwork.gestureRecognizers == nil {
// cells are reused, so only add this once
let tap = UITapGestureRecognizer(target: self, action: #selector(artworkTap(_:)))
tap.numberOfTapsRequired = 1
tap.numberOfTouchesRequired = 1
artwork.addGestureRecognizer(tap)
artwork.isUserInteractionEnabled = true
}
}
func artworkTap(_ sender: UITapGestureRecognizer) -> Void {
tapCallback?(self)
}
}
// table view class
override func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
// all you have to do is manage your data,
// no need to reload() or "re-tag" anything
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "QueueCell", for: indexPath) as! QueueCell
// your cell configuration
//let item = sungs[indexPath.section].songsIn[indexPath.row]
//cell.setup(item: item)
if indexPath.section == 2 {
// tell the cell to add the gesture recognizer
cell.addTap()
// set the "call back" closure
cell.tapCallback = {
theCell in
if let iPath = tableView.indexPath(for: theCell) {
self.deleteByTap2(tableView, indexPath: iPath)
}
}
}
return cell
}
func deleteByTap2(_ tableView: UITableView, indexPath: IndexPath) -> Void {
print("Tapped on artwork at:", indexPath)
// you now have a reference to the table view and the indexPath for the cell that
// contained the artwork image view that was tapped
}
Now your deleteByTap2() function will match the familiar didSelectRowAt function, and you can handle your deleting there.

Deselecting custom UITableViewCell that was selected by UILongPressGestureRecognizer

I have a custom UITableViewCell class My custom UITableViewCell
If user wants to select first cell, UILongPressGestureRecognizer called after that cell selected and when user had already chosen minimum one cell, then he could choose another cells without long pressing on it, just calling function didSelectRow. I tried do it by myself, but I couldn't. It was actually working but first user that was selected by long press, can't be deselected. I searched and found that I should cancel touches in view, but it didn't work for me. So cells that are called with didSelectRow are working fine, I can select and deselect, except one cell that was selected by UILongPressGesture.
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if let cell = tableView.cellForRow(at: indexPath) as? ContactCell {
if inSelectionMode {
selectUser(cell: cell, indexPath: indexPath)
} else {
if let cameraViewControl = presentingViewController as? CameraViewController {
cameraViewControl.smallView()
}
dismiss(animated: true, completion: nil)
}
}
}
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
if tableView == contactTblView {
let tap = UILongPressGestureRecognizer(target: self, action: #selector(longTapSelectUser(_:)))
tap.cancelsTouchesInView = false
cell.addGestureRecognizer(tap)
}
}
func longTapSelectUser(_ gesture: UILongPressGestureRecognizer) {
if let cell = gesture.view as? ContactCell, let indexPath = self.contactTblView.indexPath(for: cell) {
inSelectionMode = true
selectUser(cell: cell, indexPath: indexPath)
}
}

How to implement the multiple selection in uitable in swift3?

currently, i have added a long press gesture to my table view. It is working fine. Now the thing i want is that if i long press any UITableview cell that cell should get selected and after this if i tap on next cells that too should get selected too.
Below is the code:
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 10
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
let row = indexPath.row
cell.textLabel?.text = "Label"
return cell
}
#IBAction func longPress(_ guesture: UILongPressGestureRecognizer) {
if guesture.state == UIGestureRecognizerState.began {
print("Long Press")
}
}
You can set the tableView's allowsMultipleSelection property in your longPress method. Since the longPress won't trigger the cell's selection you can you can use the gesture's location in the tableView to get the initial cell that corresponds to the longPress action.
func longPress(sender:UILongPressGestureRecognizer) {
switch sender.state {
case .began:
tableView.allowsMultipleSelection = true
let point = sender.location(in: tableView)
selectCellFromPoint(point: point)
default:break
}
}
func selectCellFromPoint(point:CGPoint) {
if let indexPath = tableView.indexPathForRow(at: point) {
tableView.selectRow(at: indexPath, animated: true, scrollPosition: .none)
}
}

IndexPath in tablew view cell returned is wrong on click of a label in a UITableView

I have a label inside a table view cell.
On click of the label I want to segue to another view controller after retrieving the correct indexPath.
I have added a gestureRecognizer to the label. On click of the label it returns the wrong indexPath , it does not return the index Path of the cell in which the label is.
How can I solve this problem . Any help will be appreciated. Thank you
Following is my code
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
{
cell = tableView.dequeueReusableCell(withIdentifier: "ViewControllerCell", for: indexPath) as! TableCell
cell.name.text = feeds[indexPath.row].name
nameClicked()
return cell
}
func nameClicked(){
cell.name.isUserInteractionEnabled = true
let gestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(TrendViewController.handleTap(gestureRecognizer:)))
cell.name.addGestureRecognizer(gestureRecognizer)
}
func handleTap(gestureRecognizer: UIGestureRecognizer) {
var touchPoint = cell.name.convert(CGPoint.zero, to: self.tableView)
var clickedLabelIndexPath = tableView.indexPathForRow(at: touchPoint)!
nameFromView = feeds[clickedLabelIndexPath.row].name
print("IndexPath at touch",clickedLabelIndexPath)
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let vc = storyboard.instantiateViewController(withIdentifier: "profile") as! ProfileViewController
vc.clickedLabelIndexPath = clickedLabelIndexPath
vc.nameFromView = feeds[clickedLabelIndexPath.row].name
self.navigationController?.pushViewController(vc, animated: true)
}
You have declare cell as instance property in your class,so you are adding gesture to the same cell's label. So create new cell using dequeueReusableCell and changed your method nameClicked by adding argument of type cell.
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
//create new cell
let cell = tableView.dequeueReusableCell(withIdentifier: "ViewControllerCell", for: indexPath) as! TableCell
cell.name.text = feeds[indexPath.row].name
nameClicked(cell)
return cell
}
func nameClicked(_ cell: TableCell){
cell.name.isUserInteractionEnabled = true
let gestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(TrendViewController.handleTap(gestureRecognizer:)))
cell.name.addGestureRecognizer(gestureRecognizer)
}
Now change your handleTap method like this to get the correct indexPath.
func handleTap(_ gestureRecognizer: UIGestureRecognizer) {
let point = self.tableView.convert(CGPoint.zero, from: gestureRecognizer.view!)
if let indexPath = self. tableView.indexPathForRow(at: point) {
print("\(indexPath.section) \(indexPath.row)")
//Add your code here
}
}
Note: If your cell having only single cell then it is batter if you use didSelectRowAt method instead of adding gesture to label.
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
print(indexPath.row)
}
Maybe you should make condition for check if cell is touched ?
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "ViewControllerCell", for: indexPath) as! TableCell
if cell.isTouched {
cell.setTouched
}
else {
cell.setNoTouched
}
return cell
}

Resources