Long press on UITableView - ios

I would like to handle a long press on a UITableViewCell to print a "quick access menu".
Did someone already do this?
Particularly the gesture recognize on UITableView?

First add the long press gesture recognizer to the table view:
UILongPressGestureRecognizer *lpgr = [[UILongPressGestureRecognizer alloc]
initWithTarget:self action:#selector(handleLongPress:)];
lpgr.minimumPressDuration = 2.0; //seconds
lpgr.delegate = self;
[self.myTableView addGestureRecognizer:lpgr];
[lpgr release];
Then in the gesture handler:
-(void)handleLongPress:(UILongPressGestureRecognizer *)gestureRecognizer
{
CGPoint p = [gestureRecognizer locationInView:self.myTableView];
NSIndexPath *indexPath = [self.myTableView indexPathForRowAtPoint:p];
if (indexPath == nil) {
NSLog(#"long press on table view but not on a row");
} else if (gestureRecognizer.state == UIGestureRecognizerStateBegan) {
NSLog(#"long press on table view at row %ld", indexPath.row);
} else {
NSLog(#"gestureRecognizer.state = %ld", gestureRecognizer.state);
}
}
You have to be careful with this so that it doesn't interfere with the user's normal tapping of the cell and also note that handleLongPress may fire multiple times (this will be due to the gesture recognizer state changes).

I've used Anna-Karenina's answer, and it works almost great with a very serious bug.
If you're using sections, long-pressing the section title will give you a wrong result of pressing the first row on that section, I've added a fixed version below (including the filtering of dummy calls based on the gesture state, per Anna-Karenina suggestion).
- (IBAction)handleLongPress:(UILongPressGestureRecognizer *)gestureRecognizer
{
if (gestureRecognizer.state == UIGestureRecognizerStateBegan) {
CGPoint p = [gestureRecognizer locationInView:self.tableView];
NSIndexPath *indexPath = [self.tableView indexPathForRowAtPoint:p];
if (indexPath == nil) {
NSLog(#"long press on table view but not on a row");
} else {
UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:indexPath];
if (cell.isHighlighted) {
NSLog(#"long press on table view at section %d row %d", indexPath.section, indexPath.row);
}
}
}
}

Answer in Swift 5 (Continuation of Ricky's answer in Swift)
Add the UIGestureRecognizerDelegate to your ViewController
override func viewDidLoad() {
super.viewDidLoad()
//Long Press
let longPressGesture = UILongPressGestureRecognizer(target: self, action: #selector(handleLongPress))
longPressGesture.minimumPressDuration = 0.5
self.tableView.addGestureRecognizer(longPressGesture)
}
And the function:
#objc func handleLongPress(longPressGesture: UILongPressGestureRecognizer) {
let p = longPressGesture.location(in: self.tableView)
let indexPath = self.tableView.indexPathForRow(at: p)
if indexPath == nil {
print("Long press on table view, not row.")
} else if longPressGesture.state == UIGestureRecognizer.State.began {
print("Long press on row, at \(indexPath!.row)")
}
}

Here are clarified instruction combining Dawn Song's answer and Marmor's answer.
Drag a long Press Gesture Recognizer and drop it into your Table Cell. It will jump to the bottom of the list on the left.
Then connect the gesture recognizer the same way you would connect a button.
Add the code from Marmor in the the action handler
- (IBAction)handleLongPress:(UILongPressGestureRecognizer *)sender {
if (sender.state == UIGestureRecognizerStateBegan) {
CGPoint p = [sender locationInView:self.tableView];
NSIndexPath *indexPath = [self.tableView indexPathForRowAtPoint:p];
if (indexPath == nil) {
NSLog(#"long press on table view but not on a row");
} else {
UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:indexPath];
if (cell.isHighlighted) {
NSLog(#"long press on table view at section %d row %d", indexPath.section, indexPath.row);
}
}
}
}

Looks to be more efficient to add the recognizer directly to the cell as shown here:
Tap&Hold for TableView Cells, Then and Now
(scroll to the example at the bottom)

Answer in Swift:
Add delegate UIGestureRecognizerDelegate to your UITableViewController.
Within UITableViewController:
override func viewDidLoad() {
super.viewDidLoad()
let longPressGesture:UILongPressGestureRecognizer = UILongPressGestureRecognizer(target: self, action: "handleLongPress:")
longPressGesture.minimumPressDuration = 1.0 // 1 second press
longPressGesture.delegate = self
self.tableView.addGestureRecognizer(longPressGesture)
}
And the function:
func handleLongPress(longPressGesture:UILongPressGestureRecognizer) {
let p = longPressGesture.locationInView(self.tableView)
let indexPath = self.tableView.indexPathForRowAtPoint(p)
if indexPath == nil {
print("Long press on table view, not row.")
}
else if (longPressGesture.state == UIGestureRecognizerState.Began) {
print("Long press on row, at \(indexPath!.row)")
}
}

I put together a little category on UITableView based on Anna Karenina's excellent answer.
Like this you'll have a convenient delegate method like you're used to when dealing with regular table views. Check it out:
// UITableView+LongPress.h
#import <UIKit/UIKit.h>
#protocol UITableViewDelegateLongPress;
#interface UITableView (LongPress) <UIGestureRecognizerDelegate>
#property(nonatomic,assign) id <UITableViewDelegateLongPress> delegate;
- (void)addLongPressRecognizer;
#end
#protocol UITableViewDelegateLongPress <UITableViewDelegate>
- (void)tableView:(UITableView *)tableView didRecognizeLongPressOnRowAtIndexPath:(NSIndexPath *)indexPath;
#end
// UITableView+LongPress.m
#import "UITableView+LongPress.h"
#implementation UITableView (LongPress)
#dynamic delegate;
- (void)addLongPressRecognizer {
UILongPressGestureRecognizer *lpgr = [[UILongPressGestureRecognizer alloc]
initWithTarget:self action:#selector(handleLongPress:)];
lpgr.minimumPressDuration = 1.2; //seconds
lpgr.delegate = self;
[self addGestureRecognizer:lpgr];
}
- (void)handleLongPress:(UILongPressGestureRecognizer *)gestureRecognizer
{
CGPoint p = [gestureRecognizer locationInView:self];
NSIndexPath *indexPath = [self indexPathForRowAtPoint:p];
if (indexPath == nil) {
NSLog(#"long press on table view but not on a row");
}
else {
if (gestureRecognizer.state == UIGestureRecognizerStateBegan) {
// I am not sure why I need to cast here. But it seems to be alright.
[(id<UITableViewDelegateLongPress>)self.delegate tableView:self didRecognizeLongPressOnRowAtIndexPath:indexPath];
}
}
}
If you want to use this in a UITableViewController, you probably need to subclass and conform to the new protocol.
It works great for me, hope it helps others!

Swift 3 answer, using modern syntax, incorporating other answers, and eliminating unneeded code.
override func viewDidLoad() {
super.viewDidLoad()
let recognizer = UILongPressGestureRecognizer(target: self, action: #selector(tablePressed))
tableView.addGestureRecognizer(recognizer)
}
#IBAction func tablePressed(_ recognizer: UILongPressGestureRecognizer) {
let point = recognizer.location(in: tableView)
guard recognizer.state == .began,
let indexPath = tableView.indexPathForRow(at: point),
let cell = tableView.cellForRow(at: indexPath),
cell.isHighlighted
else {
return
}
// TODO
}

Just add UILongPressGestureRecognizer to the given prototype cell in storyboard, then pull the gesture to the viewController's .m file to create an action method.
I made it as I said.

Use the UITouch timestamp property in touchesBegan to launch a timer or stop it when touchesEnded got fired

Related

UICollectionView with gesture recogniser

I have a UICollectionView which has a long press gesture attached to it. It works fine when i'm pressing on a cell but if the touched area isn't a cell the app crashes with EXC_BREAKPOINT
It crashes on the
if let indexPath : NSIndexPath = collectView.indexPathForItemAtPoint(point)! {
line. I believe that i need to check that the point is actually a cell but i'm not sure what to check for
the code is as follows
#IBAction func longPressCell(sender: UILongPressGestureRecognizer) {
if (sender.state == UIGestureRecognizerState.Began) {
if let point : CGPoint = sender.locationInView(self.collectionView) {
if let collectView = self.collectionView {
if let indexPath : NSIndexPath = collectView.indexPathForItemAtPoint(point)! {
let adopt : UserPet = self.fetchedResultsController.objectAtIndexPath(indexPath) as! UserPet
NSLog("Adopt: \(adopt)")
}
}
}
}
}
collectView.indexPathForItemAtPoint(point) != nil {
Solved it

How to get the indexpath.row when a UIImageView in a cell is tapped?

swift: how to get the indexpath.row when a button in a cell is tapped?
This link is an answer for when a button is tapped, and if my question is not possible I'll just use a button and put the image on it. I was just wondering if it is possible to this with tapping a UIImageView instead of a button. I tried the exact answer with a UIImageView instead of a UIButton and I got this error
"fatal error: unexpectedly found nil while unwrapping an Optional value".
cell.imagePosted is a UIImageView
let tapGR = UITapGestureRecognizer(target: self,action:Selector("imageTapped:"))
cell.imagePosted.userInteractionEnabled = true
cell.imagePosted.addGestureRecognizer(tapGR);
func imageTapped(img: AnyObject)
{
if let imgView = img as? UIImageView {
if let superView = imgView.superview {
if let cell = superView.superview as? CellCustom {
indexPath2 = self.tableView.indexPathForCell(cell)
}
}
}
print(indexPath2.row)
}
this might help you,
add an UITapGestureRecognizer to UIImageView
You can store indexpath.row in tag property of UIImageView and access that tag on UITapGestureRecognizer event
for example (Objective-C) :
UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(handleImageTap:)];
tap.cancelsTouchesInView = YES;
tap.numberOfTapsRequired = 1;
[cell.imageView addGestureRecognizer:tap];
cell.imageView.tag = indexPath.row;
and get indexpath.row
-(void)handleImageTap:(UITapGestureRecognizer *)gestureRecognizer{
UIView* view = gestureRecognizer.view;
CGPoint loc = [gestureRecognizer locationInView:view];
NSInteger indexpath = [view hitTest:loc withEvent:nil].tag;
NSLog(#"%ld",(long)indexpath);
}
You can create a protocol with a method like imageViewInCellTapped(cell:YourCellType)and a delegate property in the cell. In cellForRowAtIndexPath set the controller the delegate of each cell and implement the method from the protocol. When something happens in your cell like a button or image is tapped you can call delegate?.imageViewInCellTapped(self) where self is the cell then in your controller where this method is implemented you get the index using indexPathForCell method of the tableView.
Here is Madan's code in swift if anybody is interested:
func imageTapped(gestureRecognizer: UITapGestureRecognizer) {
var view: UIView!
var loc: CGPoint!
view = gestureRecognizer.view
loc = gestureRecognizer.locationInView(view)
var indexPath: NSInteger!
indexPath = (view.hitTest(loc, withEvent: nil)?.tag)!
print(indexPath)
}
Hope this code helps to get indexpath for tapped cell swift 3:
func nameTapped(_ sender:UITapGestureRecognizer)
{
var pointVAlue = CGPoint()
pointVAlue = sender.location(in: infoTable)
var indexPath = IndexPath()
indexPath = infoTable.indexPathForRow(at: pointVAlue)!
print(indexPath.row)
/* if indexPath != nil {
let userEnt = ratingArray.object(at: indexPath.row)as! RateEntity
let detailVc = AssociateDetailViewController()
if ((userEnt.userId as String).isEmpty == false )
{
detailVc.m_otherUserId = userEnt.userId as String
self.navController!.pushViewController(detailVc, animated: true)
}
}*/
}

Tab Gesture Recognizer for specific view

I am trying to use tap gesture recognizer to hide keyboard and drop down table view (which is created programatically in another view and is called when needed). The code I used in ViewDidLoad is
override func viewDidLoad() {
super.viewDidLoad()
tap = UITapGestureRecognizer(target: self, action:Selector("DismissKeyboard"))
view.addGestureRecognizer(tap!) }
and DismissKeyboard function is
func DismissKeyboard(){
view.endEditing(true)
subviewSchool.removeFromSuperview()
subviewPosition.removeFromSuperview()
}
Button action to call Dropdown Table View is
#IBAction func dropDownPosition(sender: AnyObject) {
var frameForDropDownViewPosition = CGRect()
var framePosition = selectPositionTextField.frame
frameForDropDownViewPosition.origin.x = framePosition.origin.x
frameForDropDownViewPosition.origin.y = studentCell.frame.origin.y + framePosition.origin.y + framePosition.size.height
frameForDropDownViewPosition.size.width = framePosition.size.width
frameForDropDownViewPosition.size.height = 300
subviewPosition = DropDownView(frame: frameForDropDownViewPosition)
subviewPosition.delegate = self
subviewPosition.indicator = "positionStudent"
subviewPosition.checkposition = schoolKeyId
subviewPosition.schoolInfoArr = schoolInfoArr
self.view.addSubview(subviewPosition)
}
But the problem is that Tab Gesture did work but I am Unable to Select the contain of Drop Down view (want to perform certain task when called did select row at index path) as tap gesture is not allowing me to do so.
How can I remove Tab Gesture from Drop Down Table View (or is there an alternative way?), as I can remove Tab Gesture from all view using
self.view.removeGestureRecognizer(tap!)
but not from specific view (which is not as plan), so that I can do my work as I desire. I am using Swift
Thank you
Add gestureDelegate:
UIGestureRecognizerDelegate
In ViewDidLoad set tap delegate:
tap.delegate = self
Then call this delegate
Swift 2
func gestureRecognizer(gestureRecognizer: UIGestureRecognizer, shouldReceiveTouch touch: UITouch) -> Bool {
let p = touch.locationInView(view)
if CGRectContainsPoint(DropDownView.frame, p) {
return false
}
return true
}
Swift 4
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
let p = touch.location(in: view)
if DropDownView.frame.contains(p) {
return false
}
return true
}
Drag and drop, the TapGestureRecognizer on to the ViewController in Main.Storyboard you want this
then go to documents outlet, select the TapGestureRecognizer, Control drag it to create IBOutlet in the ViewController.swift
create an outlet for TapGestureRecognizer, name it "tap"
#IBOutlet var tap: UITapGestureRecognizer!
add UIGestureRecognizerDelegate to the ViewController
UIGestureRecognizerDelegate
in viewDidLoad() or in the #IBAction of your interest
tap.delegate = self
Then call this delegate function
func gestureRecognizer(gestureRecognizer: UIGestureRecognizer, shouldReceiveTouch touch: UITouch) -> Bool {
let p = touch.locationInView(view)
if CGRectContainsPoint(DropDownView.frame, p) {
return false
}
return true
}
if you are using the textFiled try by using textFiledDelegate Method for dismissing keyboard
-(BOOL) textFieldShouldReturn:(UITextField *)textField
{
[textField resignFirstResponder];
return YES;
}
If you want to recognise Tap gesture for multiple view. You need to add Tap Gesture SELECTOR for multiple views.
Please check below code hope it works for you.
- (void)viewDidLoad {
[super viewDidLoad];
//Added Tap Gesture to remove keyboard
UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(dismissKeyboard)];
[self.view addGestureRecognizer:tap];
UITapGestureRecognizer *singleTapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(processSingleTap:)];
[singleTapGesture setNumberOfTapsRequired:1];
[singleTapGesture setNumberOfTouchesRequired:1];
[self.tableViewObj addGestureRecognizer:singleTapGesture];
}
-(void)dismissKeyboard
{
[[[UIAlertView alloc]initWithTitle:#"Keyboard" message:#"Dismiss keyboard here..." delegate:nil cancelButtonTitle:#"OK" otherButtonTitles:nil, nil] show];
}
-(void)processSingleTap:(UITapGestureRecognizer*)gesture
{
CGPoint pointInTableView = [gesture locationInView:self.tableViewObj];
NSIndexPath *selectedIndexPath = [self.tableViewObj indexPathForRowAtPoint:pointInTableView];
UITableViewCell *selectedCell = (UITableViewCell*)[self.tableViewObj cellForRowAtIndexPath:selectedIndexPath];
if(selectedCell){
[[[UIAlertView alloc]initWithTitle:#"Cell Selected" message:[NSString stringWithFormat:#"Cell Selected Index...%#",#(selectedIndexPath.row)] delegate:nil cancelButtonTitle:#"OK" otherButtonTitles:nil, nil] show];
}
else
{
NSLog(#"Cell not selected tap of table view ...");
}
}
-(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return 10;
}
-(UITableViewCell*)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if(!cell)
{
cell = [[UITableViewCell alloc]initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
}
cell.textLabel.text = [NSString stringWithFormat:#"Cell %#",#(indexPath.row)];
return cell;
}
You can check if it was tapped inside UITableView.
Just have a look at this answer How get UITableView IndexPath from UITableView iphone?. It's a much better and a simple way.

adding UILongPressGestureRecognizer to last tableview cell fails to detect locationInView

I'm trying to add a UILongPressGestureRecongnizer to each tableview cell. The following code works in all cases except the last cell:
ViewDidLoad:
let longpress = UILongPressGestureRecognizer(target: self, action: "handleLongPress:")
longpress.minimumPressDuration = 0.35
tableView.addGestureRecognizer(longpress)
handleLongPress:
func handleLongPress(sender:UILongPressGestureRecognizer!) {
let localLongPress = sender as UILongPressGestureRecognizer
var locationInView = localLongPress.locationInView(tableView)
// returns nil in the case of last cell
// but strangely only on Ended State
var indexPath = tableView.indexPathForRowAtPoint(locationInView)
if indexPath != nil {
var cell = self.tableView.cellForRowAtIndexPath(indexPath!) as MyCellTableViewCell
println("Long press Block .................");
if (sender.state == UIGestureRecognizerState.Ended) {
println("Long press Ended");
} else if (sender.state == UIGestureRecognizerState.Began) {
println("Long press detected.");
}
}
}
In the case of my last cell, I always see the UIGestureRecognizerState.Began but I do not see the UIGestureRecognizerState.Ended. I see both states for every cell, except the last tableviewcell. tableview.indexPathForRowAtPoint() resolves to nil in this case. What am I doing wrong?

Dismiss modal form sheet view on outside tap iOS 8

I've been trying to dismiss the modal form sheet view on outside tap on iOS 8 with no luck,
I've tried this code
UITapGestureRecognizer *recognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(handleTapBehind:)];
[recognizer setNumberOfTapsRequired:1];
recognizer.cancelsTouchesInView = NO; //So the user can still interact with controls in the modal view
[self.view.window addGestureRecognizer:recognizer];
- (void)handleTapBehind:(UITapGestureRecognizer *)sender
{
if (sender.state == UIGestureRecognizerStateEnded)
{
CGPoint location = [sender locationInView:nil]; //Passing nil gives us coordinates in the window
//Then we convert the tap's location into the local view's coordinate system, and test to see if it's in or outside. If outside, dismiss the view.
if (![self.view pointInside:[self.view convertPoint:location fromView:self.view.window] withEvent:nil])
{
// Remove the recognizer first so it's view.window is valid.
[self.view.window removeGestureRecognizer:sender];
[self dismissModalViewControllerAnimated:YES];
}
}
}
But it doesn't detect outside view clicks, any suggestions ?
There are actually two problems in iOS 8. First, the gesture recognition does not begin.
I solved this by adding the UIGestureRecognizerDelegate protocol and implementing
-(BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer*)otherGestureRecognizer
{
return YES;
}
Also, don't forget to register the delegate with
recognizer.delegate = self;
Now the gesture recognizer should recognize gestures and the target method (handleTapBehind:) will be called.
Here comes the second problem in iOS 8: locationInView: doesn't seem to take the device orientation into account if nil is passed as a view. Instead, passing the root view works.
Here's my target code that seems to work for iOS 7.1 and 8.0:
if (sender.state == UIGestureRecognizerStateEnded) {
UIView *rootView = self.view.window.rootViewController.view;
CGPoint location = [sender locationInView:rootView];
if (![self.view pointInside:[self.view convertPoint:location fromView:rootView] withEvent:nil]) {
[self dismissViewControllerAnimated:YES completion:^{
[self.view.window removeGestureRecognizer:sender];
}];
}
}
In iOS 8, You can look at using the new UIPresentationController class. It gives you better control over the container around your custom view controller presentation (allowing you to correctly add a gesture recogniser of your own).
Here is a link to quite a simple tutorial as well: http://dativestudios.com/blog/2014/06/29/presentation-controllers/
Then add the dimming view tap-to-dismiss:
UITapGestureRecognizer *singleFingerTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(handleSingleTap:)];
[self.dimmingView addGestureRecognizer:singleFingerTap];
- (void)handleSingleTap:(UITapGestureRecognizer *)recognizer {
[self.presentingViewController dismissViewControllerAnimated:YES completion:nil];
}
Swift 3.1 solution that works in both portrait and landscape.
class TapBehindModalViewController: UIViewController, UIGestureRecognizerDelegate {
private var tapOutsideRecognizer: UITapGestureRecognizer!
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
if(self.tapOutsideRecognizer == nil) {
self.tapOutsideRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.handleTapBehind))
self.tapOutsideRecognizer.numberOfTapsRequired = 1
self.tapOutsideRecognizer.cancelsTouchesInView = false
self.tapOutsideRecognizer.delegate = self
self.view.window?.addGestureRecognizer(self.tapOutsideRecognizer)
}
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
if(self.tapOutsideRecognizer != nil) {
self.view.window?.removeGestureRecognizer(self.tapOutsideRecognizer)
self.tapOutsideRecognizer = nil
}
}
func close(sender: AnyObject) {
self.dismiss(animated: true, completion: nil)
}
// MARK: - Gesture methods to dismiss this with tap outside
func handleTapBehind(sender: UITapGestureRecognizer) {
if (sender.state == UIGestureRecognizerState.ended) {
let location: CGPoint = sender.location(in: self.view)
if (!self.view.point(inside: location, with: nil)) {
self.view.window?.removeGestureRecognizer(sender)
self.close(sender: sender)
}
}
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
}

Resources