I have a view hierarchy that looks something like this:
UIView (A)
UIView > UIImageView
UIView > UIView (B)
UIView > UIView (B) > Rounded Rect Button
UIView > UIView (B) > UIImageView
UIView > UIView (B) > UILabel
I've attached gesture recognizer(s) to my UIView (B). The problem that i'm facing is that i don't get any actions for the Rounded Rect Button which is inside the UIView (B). The singleTap gesture recognizer captures/overrides the button's Touch Up Inside event.
How can i make it work? I thought that the responder chain hierarchy will make sure that the button touch event will be given preference, and it WILL get triggered! What am i missing?
Here's some related code:
#pragma mark -
#pragma mark View lifecycle (Gesture recognizer setup)
- (void)viewDidLoad {
[super viewDidLoad];
// double tap gesture recognizer
UITapGestureRecognizer *dtapGestureRecognize = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(doubleTapGestureRecognizer:)];
dtapGestureRecognize.delegate = self;
dtapGestureRecognize.numberOfTapsRequired = 2;
[self.viewB addGestureRecognizer:dtapGestureRecognize];
// single tap gesture recognizer
UITapGestureRecognizer *tapGestureRecognize = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(singleTapGestureRecognizer:)];
tapGestureRecognize.delegate = self;
tapGestureRecognize.numberOfTapsRequired = 1;
[tapGestureRecognize requireGestureRecognizerToFail:dtapGestureRecognize];
[self.viewB addGestureRecognizer:tapGestureRecognize];
// add gesture recodgnizer to the grid view to start the edit mode
UILongPressGestureRecognizer *pahGestureRecognizer = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:#selector(longPressGestureRecognizerStateChanged:)];
pahGestureRecognizer.delegate = self;
pahGestureRecognizer.minimumPressDuration = 0.5;
[self.viewB addGestureRecognizer:pahGestureRecognizer];
[dtapGestureRecognize release];
[tapGestureRecognize release];
[pahGestureRecognizer release];
}
#pragma mark -
#pragma mark Button actions
- (IBAction)buttonTouchUpInside:(id)sender {
NSLog(#"%s, %#", __FUNCTION__, sender);
}
#pragma mark -
#pragma mark Gesture recognizer actions
- (void)singleTapGestureRecognizer:(UIGestureRecognizer *)gestureRecognizer {
NSLog(#"%s", __FUNCTION__);
}
- (void)doubleTapGestureRecognizer:(UIGestureRecognizer *)gestureRecognizer {
NSLog(#"%s", __FUNCTION__);
}
- (void)longPressGestureRecognizerStateChanged:(UIGestureRecognizer *)gestureRecognizer {
switch (gestureRecognizer.state) {
case UIGestureRecognizerStateEnded: {
NSLog(#"%s", __FUNCTION__);
break;
}
default:
break;
}
}
In the "shouldReceiveTouch" method you should add a condition that will return NO if the touch is in the button.
This is from apple SimpleGestureRecognizers example.
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch {
// Disallow recognition of tap gestures in the segmented control.
if ((touch.view == yourButton)) {//change it to your condition
return NO;
}
return YES;
}
hope it will help
Edit
As Daniel noted you must conform to UIGestureRecognizerDelegate for it to work.
shani
I also had the same problem , then i tried with
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch{
if ([touch.view isKindOfClass:[UIButton class]]) { //change it to your condition
return NO;
}
return YES;
}
It is working now perfectly.........
Generally speaking, we use below delegate method to avoid the touch in all kinds of UIControls:
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch {
if (([touch.view isKindOfClass:[UIControl class]])) {
return NO;
}
return YES;
}
Note: DO NOT do this check (check the recognizer.view class type) the gestureRecognizerShouldBegin, it won't work.
Here is a Swift 3.0 version:
extension UIViewController: UIGestureRecognizerDelegate {
public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
if touch.view is UIButton {
return false
}
return true
}
Don't forget to:
Make your tapper object delegate to self (e.g: tapper.delegate = self)
The best solution is to my mind using code snippet below:
-(BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch {
CGPoint touchLocation = [touch locationInView:self.view];
return !CGRectContainsPoint(self.menuButton.frame, touchLocation);
}
Here is a Swift version:
func gestureRecognizer(gestureRecognizer: UIGestureRecognizer, shouldReceiveTouch touch: UITouch) -> Bool {
if (touch.view!.isKindOfClass(UIButton)) {
return false
}
return true
}
Don't forget to:
Make you class conform to UIGestureRecognizerDelegate
Make your tapper object delegate to self (e.g: tapper.delegate = self)
for Swift 5 inside the delegate you can just add:
return (touch.view is UIButton) ? false : true
Swift version of Vivienne answer:
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
let location = touch.location(in: view)
return !someView.frame.contains(location)
}
My case was the following :
UIView (with tap gesture)
├-> UIView (named "textFieldView")
├-> UIControl
├-> UITextField
├-> UIView (over the textField, with a tap gesture)
╭──────────────────────────────╮
│ ╭──────────────────────────╮ │
│ │ ╭──╮ ┌╴╶╴╶╴╶╴╶╴╶╴╶╴╶╴╶┐ │ │
│ │ ╰──╯ └╴╶╴╶╴╶╴╶╴╶╴╶╴╶╴╶┘ │ │
│ ╰──────────────────────────╯ │
└──────────────────────────────┘
The tapGesture on the upper UIView was causing a conflict with the UIControl target. The target was called if the target was setup with touchDown (but not called for touchUpInside) or if I was changing the inheritance from UIControl to UIButton while keeping the target setup on event touchUpInside.
What I do to solve that is :
upper view is setup as delegate of its tap gesture.
Function func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool is implemented (in upper UIView) like that :
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
let touchLocationRelatedToTextFieldView = touch.location(in: textFieldView)
return textFieldView.frame.contains(touchLocationRelatedToTextFieldView) == false
}
With that delegate function implemented I'm now able to keep my UIControl and the target setup on touchUpInside! :)
Related
I have written my own function to scroll text fields up when the keyboard shows up. In order to dismiss the keyboard by tapping away from the text field, I've created a UITapGestureRecognizer that takes care of resigning first responder on the text field when tapping away.
Now I've also created an autocomplete for the textfield that creates a UITableView just below the text field and populates it with items as the user enters text.
However, when selecting one of the entries in the auto completed table, didSelectRowAtIndexPath does not get called. Instead, it seems that the tap gesture recognizer is getting called and just resigns first responder.
I'm guessing there's some way to tell the tap gesture recognizer to keep passing the tap message on down to the UITableView, but I can't figure out what it is. Any help would be very appreciated.
Ok, finally found it after some searching through gesture recognizer docs.
The solution was to implement UIGestureRecognizerDelegate and add the following:
#pragma mark UIGestureRecognizerDelegate methods
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch
{
if ([touch.view isDescendantOfView:autocompleteTableView]) {
// Don't let selections of auto-complete entries fire the
// gesture recognizer
return NO;
}
return YES;
}
That took care of it. Hopefully this will help others as well.
The easiest way to solve this problem is to:
UITapGestureRecognizer *tapRec = [[UITapGestureRecognizer alloc]
initWithTarget:self action:#selector(tap:)];
[tapRec setCancelsTouchesInView:NO];
This lets the UIGestureRecognizer recognize the tap and also pass the touch to the next responder. An unintended consequence of this method is if you have a UITableViewCell on-screen that pushes another view controller. If the user taps the row to dismiss the keyboard, both the keyboard and the push will be recognized. I doubt this is what you intend, but this method is adequate for many situations.
Also, expanding on Robert's answer, if you have a pointer to the tableview in question, then you can directly compare its class instead of having to convert to a string and hope Apple doesn't change the nomenclature:
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer
shouldReceiveTouch:(UITouch *)touch
{
if([touch.view class] == tableview.class){
return //YES/NO
}
return //YES/NO
}
Remember, you must also declare the UIGestureRecognizer to have a delegate with this code in it.
Set cancelsTouchesInView of your recognizer to false. Otherwise, it "consumes" the touch for itself, and does not pass it on to the table view. That's why the selection event never happens.
for example in swift
let tapOnScreen: UITapGestureRecognizer = UITapGestureRecognizer(target: self, action: "CheckTheTime")
tapOnScreen.cancelsTouchesInView = false
view.addGestureRecognizer(tapOnScreen)
And for Swift (based on answer from #Jason):
class MyAwesomeClass: UIViewController, UIGestureRecognizerDelegate
private var tap: UITapGestureRecognizer!
override func viewDidLoad() {
super.viewDidLoad()
self.tap = UITapGestureRecognizer(target: self, action: "viewTapped:")
self.tap.delegate = self
self.view.addGestureRecognizer(self.tap)
}
// UIGestureRecognizerDelegate method
func gestureRecognizer(gestureRecognizer: UIGestureRecognizer, shouldReceiveTouch touch: UITouch) -> Bool {
if touch.view?.isDescendantOfView(self.tableView) == true {
return false
}
return true
}
I may have a better solution to add a tap gesture over a table view but allowing cell selection at the same time:
func gestureRecognizer(gestureRecognizer: UIGestureRecognizer, shouldReceiveTouch touch: UITouch) -> Bool {
if gestureRecognizer is UITapGestureRecognizer {
let location = touch.locationInView(tableView)
return (tableView.indexPathForRowAtPoint(location) == nil)
}
return true
}
I just look for a cell at the point of the screen where the user is tapping. If no index path is found then I let the gesture receive the touch otherwise I cancel it. For me it works great.
I think there is no need to write blocks of codes just simply set
cancelsTouchesInView to false for your gesture object ,
by default it's true and you just have to set it false .
If you are using UITapGesture object in your code and also using UIScrollView(tableview , collectionview)then set this property false
let tap: UITapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.dismissKeyboard))
tap.cancelsTouchesInView = false
view.addGestureRecognizer(tap)
A similar solution is to implement gestureRecognizer:shouldReceiveTouch: using the view's class to determine what action to take. This approach has the advantage of not masking taps in the region directly surrounding the table (these area's views still descend from the UITableView instances, but they do not represent cells).
This also has a bonus that it works with multiple tables on a single view (without adding extra code).
Caveat: there is an assumption that Apple won't change the classname.
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch
{
return ![NSStringFromClass([touch.view class]) isEqualToString:#"UITableViewCellContentView"];
}
For Swift 4.2 the solution was to implement UIGestureRecognizerDelegate and add the following:
extension ViewController : UIGestureRecognizerDelegate {
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
if touch.view!.isDescendant(of: tblView) {
return false
}
return true
}
}
when you click on table view this delegate method returns false and didSelectRowAtIndexPath method of table view working properly.
I had a different situation where I wanted the touch gesture function to be called only when the user tapped outside of the table view. If the user tapped inside the table view, then the touch gesture function shouldn't be called. Additionally, If the touch gesture function is called, it should still pass the touch event to the view that was tapped on rather than consuming it.
The resulting code is a combination of Abdulrahman Masoud's answer, and Nikolaj Nielsen's answer.
extension MyViewController: UIGestureRecognizerDelegate {
func addGestureRecognizer() {
let tapOnScreen = UITapGestureRecognizer(target: self,
action: #selector(functionToCallWhenUserTapsOutsideOfTableView))
// stop the gesture recognizer from "consuming" the touch event,
// so that the touch event can reach other buttons on view.
tapOnScreen.cancelsTouchesInView = false
tapOnScreen.delegate = self
self.view.addGestureRecognizer(tapOnScreen)
}
// if your tap event is on the menu, don't run the touch event.
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
if touch.view?.isDescendant(of: self.tableView) == true {
return false
}
return true
}
#objc func functionToCallWhenUserTapsOutsideOfTableView() {
print("user tapped outside table view")
}
}
And in the MyViewController class, the class which has the UITableView, in the onViewDidLoad(), I made sure to call addGestureRecognizer():
class MyViewController: UIViewController {
...
override func viewDidLoad() {
super.viewDidLoad()
...
self.addGestureRecognizer()
...
}
...
}
Just set cancels touches in view to false for any gesture underneath the (table/collection)View:
- Interfacebuilder
- Code
<#gestureRecognizer#>.cancelsTouchesInView = false
Swift 5, May 2020.
I have a textField and a tableView that becomes visible when I enter text.
Initial state
So I want 2 different events when I tap tableViewCell or something else.
Keyboard and tableView are being shown
First we add tapGestureRecognizer.
tap = UITapGestureRecognizer(target: self, action: #selector(viewTapped))
tap.delegate = self
view.addGestureRecognizer(tap)
#objc func viewTapped() {
view.endEditing(true)
}
Then we add the following check into UIGestureRecognizerDelegate:
extension StadtViewController: UIGestureRecognizerDelegate {
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
if touch.view?.isDescendant(of: self.tableView) == true {
return false
} else {
view.endEditing(true)
return true
}
}
}
If I want to hide keyboard first, the tableView remains visible and responsive to my taps.
enter image description here
Simple solution is using UIControl instances in UITableViewCell to get touches. You can add any views with userInteractionEnables == NO to UIControl to get taps.
For CollectionView in Swift 5:
func gestureRecognizer(gestureRecognizer: UIGestureRecognizer, shouldReceiveTouch touch: UITouch) -> Bool {
if gestureRecognizer is UITapGestureRecognizer {
let location = touch.location(in: mCollectionView)
return (mCollectionView.indexPathForItem(at: location) == nil)
}
return true
}
While it's late and many people find that the above suggestions work fine, I could not get Jason's or TMilligan's methods to work.
I have a Static tableView with multiple cells containing textFields that receive Number inputs using only the Number Keyboard. This was ideal for me:
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch {
if(![touch.view isKindOfClass:[UITableViewCell class]]){
[self.firstTF resignFirstResponder];
[self.secondTF resignFirstResponder];
[self.thirdTF resignFirstResponder];
[self.fourthTF resignFirstResponder];
NSLog(#"Touches Work ");
return NO;
}
return YES;
}
Ensure that you have implemented this <UIGestureRecognizerDelegate> in your .h file.
This line ![touch.view isKindOfClass:[UITableViewCell class]] checks whether a tableViewCell was tapped and dismisses any active keyboard.
Solution for Swift, works in 2021. For this solution you don't have to have the reference(s) to the Table View(s).
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
//return !(touch.view is UITableViewCell) <-- doesn't work, the type of the touched class is not UITableViewCell anymore
var v = touch.view
while v != nil {
if v is UITableView { return false }
v = v?.superview
}
return true
}
Usage:
let gestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(YOUR_METHOD))
gestureRecognizer.delegate = self
YOUR_VIEW.addGestureRecognizer(gestureRecognizer)
Here is my solution, which ties the recognizer's shouldReceiveTouch directly to whether the keyboard is showing.
In your tap gesture recognizer delegate:
#pragma mark - UIGestureRecognizerDelegate
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch
{
if ([PFXKeyboardStateListener sharedInstance].visible) {
return YES;
}
return NO;
}
And the PFXKeyboardStateListener.h:
#interface PFXKeyboardStateListener : NSObject
{
BOOL _isVisible;
}
+ (PFXKeyboardStateListener *)sharedInstance;
#property (nonatomic, readonly, getter=isVisible) BOOL visible;
#end
And the PFXKeyboardStateListener.m:
static PFXKeyboardStateListener *sharedInstance;
#implementation PFXKeyboardStateListener
+ (PFXKeyboardStateListener *)sharedInstance
{
return sharedInstance;
}
+ (void)load
{
#autoreleasepool {
sharedInstance = [[self alloc] init];
}
}
- (void)dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
- (BOOL)isVisible
{
return _isVisible;
}
- (void)didShow
{
_isVisible = YES;
}
- (void)didHide
{
_isVisible = NO;
}
- (id)init
{
if ((self = [super init])) {
NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
[center addObserver:self selector:#selector(didShow) name:UIKeyboardDidShowNotification object:nil];
[center addObserver:self selector:#selector(didHide) name:UIKeyboardWillHideNotification object:nil];
}
return self;
}
#end
You may want to update the singleton pattern of the keyboard listener, I haven't gotten to it yet. Hope this works for everyone else as well as it works for me. ^^
Implement this method for delegate of UIGestureRecognizer:
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer
shouldReceiveTouch:(UITouch *)touch
{
UIView *superview = touch.view;
do {
superview = superview.superview;
if ([superview isKindOfClass:[UITableViewCell class]])
return NO;
} while (superview && ![superview isKindOfClass:[UITableView class]]);
return superview != nil;
}
My case was different including a uisearchbar and uitableview on self.view. I wanted to dismiss uisearchbar keyboard by touching on the view.
var tapGestureRecognizer:UITapGestureRecognizer?
override func viewWillAppear(animated: Bool) {
tapGestureRecognizer = UITapGestureRecognizer(target: self, action:Selector("handleTap:"))
}
On UISearchBar Delegate Methods:
func searchBarShouldBeginEditing(searchBar: UISearchBar) -> Bool {
view.addGestureRecognizer(tapGestureRecognizer!)
return true
}
func searchBarShouldEndEditing(searchBar: UISearchBar) -> Bool {
view.removeGestureRecognizer(tapGestureRecognizer!)
return true
}
When user touches on self.view:
func handleTap(recognizer: UITapGestureRecognizer) {
sampleSearchBar.endEditing(true)
sampleSearchBar.resignFirstResponder()
}
In swift you can use this inside
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
if CheckTheTime() == true {
// do something
}else{
}
}
func CheckTheTime() -> Bool{
return true
}
This is my solution based on above answers...
It's worked for me...
//Create tap gesture for menu transparent view
UITapGestureRecognizer *rightTableTransparentViewTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(rightTableTransparentViewTapMethod:)];
[rightTableTransparentViewTap setCancelsTouchesInView:NO];
[_rightTableTransparentView addGestureRecognizer:rightTableTransparentViewTap];
- (void)rightTableTransparentViewTapMethod:(UITapGestureRecognizer *)recognizer {
//Write your code here
}
ISSUE:
In my case, the issue was that I originally placed a button in each collectionView cell and set the constraints to fill the cell, so that when the cell was clicked it would click the button, however the buttons function was empty so nothing was appearing to be happening.
FIX:
I fixed this by removing the button from the collection view cell.
I have the following hierarchy:
View
Subview
Each of those views has UIPanGestureRecognizer assigned. Outer pan gesture recognizer is only interested in vertical panning, so I have the delegate method implemented for that:
- (BOOL)gestureRecognizerShouldBegin:(UIPanGestureRecognizer *)panGestureRecognizer {
CGPoint velocity = [panGestureRecognizer velocityInView:panGestureRecognizer.view];
return fabs(velocity.y) > fabs(velocity.x);
}
However Subview's pan gesture always takes precedence in recognition. Is there any way to make outer pan gesture recognizer to recognize first and then if it fails, pan gesture from subview can take over?
I tried simultaneous recognition and it works, but then both recognizers start panning:
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer
shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
return YES;
}
Since iOS 7, UIGestureRecognizerDelegate Protocol has already declared two delegate methods for your need, you can either use
|gestureRecognizer:shouldRequireFailureOfGestureRecognizer:|
or
|gestureRecognizerShouldBegin:shouldBeRequiredToFailByGestureRecognizer:| but in the opposite way.
The following snippet is my UIPanGestureRecognizer delegate implementation:
- (BOOL)gestureRecognizerShouldBegin:(UIPanGestureRecognizer *)panGestureRecognizer {
CGPoint velocity = [panGestureRecognizer velocityInView:panGestureRecognizer.view];
return fabs(velocity.y) > fabs(velocity.x);
}
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldBeRequiredToFailByGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer{
if (otherGestureRecognizer == panGestureOfSubView) {
return YES;
}
return NO;
}
This code might be helpful. It's what I wrote to allow dragging for a specific view, but not for its subviews:
func gestureRecognizer(gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWithGestureRecognizer otherGestureRecognizer: UIGestureRecognizer) -> Bool {
if gestureRecognizer == longTouchGesture && otherGestureRecognizer == panTouchGesture && gestureRecognizer.view?.hitTest(gestureRecognizer.locationInView(gestureRecognizer.view), withEvent: nil) == self {
print("returning true")
return true
}
print("returning false")
return false
}
In the new iOS 7 UINavigationController, there is a swipe gesture to switch between views.
Is there a way to detect or intercept the gesture?
The interactive pop gesture recognizer is exposed through UINavigationController's interactivePopGestureRecognizer property. You can add your own controller as a target of the gesture recognizer and respond appropriately:
#implementation MyViewController
...
- (void)viewDidLoad
{
[super viewDidLoad];
[self.navigationController.interactivePopGestureRecognizer addTarget:self
action:#selector(handlePopGesture:)];
}
- (void)handlePopGesture:(UIGestureRecognizer *)gesture
{
if (gesture.state == UIGestureRecognizerStateBegan)
{
// respond to beginning of pop gesture
}
// handle other gesture states, if desired
}
...
#end
Here is Austin's answer, in Swift. Using this post to construct the selector, I found the following to work.
override func viewDidLoad() {
super.viewDidLoad()
self.navigationController?.interactivePopGestureRecognizer?.addTarget(self, action:#selector(self.handlePopGesture))
}
func handlePopGesture(gesture: UIGestureRecognizer) -> Void {
if gesture.state == UIGestureRecognizerState.Began {
// respond to beginning of pop gesture
}
}
I want to disable text editing in UITextView, only cursor should stay. I already disabled keyboard, cut-copy-paste menu and zoom edit mode. But there still one problem - if I double tapping on TextView it's selects whole word. And one more thing, how can I let cursor select any place, not only end or start of word?
I did screenshots, which better describes my problem, but cant post it because of reputation. So I hope, that you will understand what exactly I mean.
Subclass of UITextView:
#import "UIUneditableTextView.h"
#implementation UIUneditableTextView
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
// Initialization code
}
return self;
}
- (BOOL)canPerformAction:(SEL)action withSender:(id)sender
{
UIMenuController *menuController = [UIMenuController sharedMenuController];
if (menuController) {
[UIMenuController sharedMenuController].menuVisible = NO;
}
return NO;
}
-(void)addGestureRecognizerForLongPress:(UIGestureRecognizer *)gestureRecognizer
{
if ([gestureRecognizer isKindOfClass:[UILongPressGestureRecognizer class]]){
gestureRecognizer.enabled = NO;
}
[super addGestureRecognizer:gestureRecognizer];
return;
}
#end
Since you're already overloading the text view, implement gestureRecognizerShouldBegin: I did an implementation like this.
-(BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer {
// Check for gestures to prevent
if ([gestureRecognizer isKindOfClass:[UITapGestureRecognizer class]]) {
// Check for double tap
if (((UITapGestureRecognizer *)gestureRecognizer).numberOfTapsRequired == 2) {
// Prevent the double tap
return NO;
}
}
// Always anything that makes it here
return YES;
}
You might want to consider checking for the long gesture recognizer here, this is much less of a hack.
Look at the methods in UITextInput as this is where selection is controlled. You can act as the inputDelegate to find out when selectionWillChange: and override selectedTextRange to control the selection and caret.
Docs: http://developer.apple.com/library/ios/documentation/UIKit/Reference/UITextInput_Protocol/Reference/Reference.html#//apple_ref/occ/intfp/UITextInput
Here comes Swift working example
class TextView: UITextView {
override func canPerformAction(action: Selector, withSender sender: AnyObject!) -> Bool {
return false
}
override func gestureRecognizerShouldBegin(gestureRecognizer: UIGestureRecognizer!) -> Bool {
if gestureRecognizer.isKindOfClass(UITapGestureRecognizer) && ((gestureRecognizer as UITapGestureRecognizer).numberOfTapsRequired == 1) {
let touchPoint = gestureRecognizer.locationOfTouch(0, inView: self)
let cursorPosition = closestPositionToPoint(touchPoint)
selectedTextRange = textRangeFromPosition(cursorPosition, toPosition: cursorPosition)
return true
}
else {
return false
}
}
}
I have written my own function to scroll text fields up when the keyboard shows up. In order to dismiss the keyboard by tapping away from the text field, I've created a UITapGestureRecognizer that takes care of resigning first responder on the text field when tapping away.
Now I've also created an autocomplete for the textfield that creates a UITableView just below the text field and populates it with items as the user enters text.
However, when selecting one of the entries in the auto completed table, didSelectRowAtIndexPath does not get called. Instead, it seems that the tap gesture recognizer is getting called and just resigns first responder.
I'm guessing there's some way to tell the tap gesture recognizer to keep passing the tap message on down to the UITableView, but I can't figure out what it is. Any help would be very appreciated.
Ok, finally found it after some searching through gesture recognizer docs.
The solution was to implement UIGestureRecognizerDelegate and add the following:
#pragma mark UIGestureRecognizerDelegate methods
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch
{
if ([touch.view isDescendantOfView:autocompleteTableView]) {
// Don't let selections of auto-complete entries fire the
// gesture recognizer
return NO;
}
return YES;
}
That took care of it. Hopefully this will help others as well.
The easiest way to solve this problem is to:
UITapGestureRecognizer *tapRec = [[UITapGestureRecognizer alloc]
initWithTarget:self action:#selector(tap:)];
[tapRec setCancelsTouchesInView:NO];
This lets the UIGestureRecognizer recognize the tap and also pass the touch to the next responder. An unintended consequence of this method is if you have a UITableViewCell on-screen that pushes another view controller. If the user taps the row to dismiss the keyboard, both the keyboard and the push will be recognized. I doubt this is what you intend, but this method is adequate for many situations.
Also, expanding on Robert's answer, if you have a pointer to the tableview in question, then you can directly compare its class instead of having to convert to a string and hope Apple doesn't change the nomenclature:
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer
shouldReceiveTouch:(UITouch *)touch
{
if([touch.view class] == tableview.class){
return //YES/NO
}
return //YES/NO
}
Remember, you must also declare the UIGestureRecognizer to have a delegate with this code in it.
Set cancelsTouchesInView of your recognizer to false. Otherwise, it "consumes" the touch for itself, and does not pass it on to the table view. That's why the selection event never happens.
for example in swift
let tapOnScreen: UITapGestureRecognizer = UITapGestureRecognizer(target: self, action: "CheckTheTime")
tapOnScreen.cancelsTouchesInView = false
view.addGestureRecognizer(tapOnScreen)
And for Swift (based on answer from #Jason):
class MyAwesomeClass: UIViewController, UIGestureRecognizerDelegate
private var tap: UITapGestureRecognizer!
override func viewDidLoad() {
super.viewDidLoad()
self.tap = UITapGestureRecognizer(target: self, action: "viewTapped:")
self.tap.delegate = self
self.view.addGestureRecognizer(self.tap)
}
// UIGestureRecognizerDelegate method
func gestureRecognizer(gestureRecognizer: UIGestureRecognizer, shouldReceiveTouch touch: UITouch) -> Bool {
if touch.view?.isDescendantOfView(self.tableView) == true {
return false
}
return true
}
I may have a better solution to add a tap gesture over a table view but allowing cell selection at the same time:
func gestureRecognizer(gestureRecognizer: UIGestureRecognizer, shouldReceiveTouch touch: UITouch) -> Bool {
if gestureRecognizer is UITapGestureRecognizer {
let location = touch.locationInView(tableView)
return (tableView.indexPathForRowAtPoint(location) == nil)
}
return true
}
I just look for a cell at the point of the screen where the user is tapping. If no index path is found then I let the gesture receive the touch otherwise I cancel it. For me it works great.
I think there is no need to write blocks of codes just simply set
cancelsTouchesInView to false for your gesture object ,
by default it's true and you just have to set it false .
If you are using UITapGesture object in your code and also using UIScrollView(tableview , collectionview)then set this property false
let tap: UITapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.dismissKeyboard))
tap.cancelsTouchesInView = false
view.addGestureRecognizer(tap)
A similar solution is to implement gestureRecognizer:shouldReceiveTouch: using the view's class to determine what action to take. This approach has the advantage of not masking taps in the region directly surrounding the table (these area's views still descend from the UITableView instances, but they do not represent cells).
This also has a bonus that it works with multiple tables on a single view (without adding extra code).
Caveat: there is an assumption that Apple won't change the classname.
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch
{
return ![NSStringFromClass([touch.view class]) isEqualToString:#"UITableViewCellContentView"];
}
For Swift 4.2 the solution was to implement UIGestureRecognizerDelegate and add the following:
extension ViewController : UIGestureRecognizerDelegate {
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
if touch.view!.isDescendant(of: tblView) {
return false
}
return true
}
}
when you click on table view this delegate method returns false and didSelectRowAtIndexPath method of table view working properly.
I had a different situation where I wanted the touch gesture function to be called only when the user tapped outside of the table view. If the user tapped inside the table view, then the touch gesture function shouldn't be called. Additionally, If the touch gesture function is called, it should still pass the touch event to the view that was tapped on rather than consuming it.
The resulting code is a combination of Abdulrahman Masoud's answer, and Nikolaj Nielsen's answer.
extension MyViewController: UIGestureRecognizerDelegate {
func addGestureRecognizer() {
let tapOnScreen = UITapGestureRecognizer(target: self,
action: #selector(functionToCallWhenUserTapsOutsideOfTableView))
// stop the gesture recognizer from "consuming" the touch event,
// so that the touch event can reach other buttons on view.
tapOnScreen.cancelsTouchesInView = false
tapOnScreen.delegate = self
self.view.addGestureRecognizer(tapOnScreen)
}
// if your tap event is on the menu, don't run the touch event.
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
if touch.view?.isDescendant(of: self.tableView) == true {
return false
}
return true
}
#objc func functionToCallWhenUserTapsOutsideOfTableView() {
print("user tapped outside table view")
}
}
And in the MyViewController class, the class which has the UITableView, in the onViewDidLoad(), I made sure to call addGestureRecognizer():
class MyViewController: UIViewController {
...
override func viewDidLoad() {
super.viewDidLoad()
...
self.addGestureRecognizer()
...
}
...
}
Just set cancels touches in view to false for any gesture underneath the (table/collection)View:
- Interfacebuilder
- Code
<#gestureRecognizer#>.cancelsTouchesInView = false
Swift 5, May 2020.
I have a textField and a tableView that becomes visible when I enter text.
Initial state
So I want 2 different events when I tap tableViewCell or something else.
Keyboard and tableView are being shown
First we add tapGestureRecognizer.
tap = UITapGestureRecognizer(target: self, action: #selector(viewTapped))
tap.delegate = self
view.addGestureRecognizer(tap)
#objc func viewTapped() {
view.endEditing(true)
}
Then we add the following check into UIGestureRecognizerDelegate:
extension StadtViewController: UIGestureRecognizerDelegate {
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
if touch.view?.isDescendant(of: self.tableView) == true {
return false
} else {
view.endEditing(true)
return true
}
}
}
If I want to hide keyboard first, the tableView remains visible and responsive to my taps.
enter image description here
Simple solution is using UIControl instances in UITableViewCell to get touches. You can add any views with userInteractionEnables == NO to UIControl to get taps.
For CollectionView in Swift 5:
func gestureRecognizer(gestureRecognizer: UIGestureRecognizer, shouldReceiveTouch touch: UITouch) -> Bool {
if gestureRecognizer is UITapGestureRecognizer {
let location = touch.location(in: mCollectionView)
return (mCollectionView.indexPathForItem(at: location) == nil)
}
return true
}
While it's late and many people find that the above suggestions work fine, I could not get Jason's or TMilligan's methods to work.
I have a Static tableView with multiple cells containing textFields that receive Number inputs using only the Number Keyboard. This was ideal for me:
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch {
if(![touch.view isKindOfClass:[UITableViewCell class]]){
[self.firstTF resignFirstResponder];
[self.secondTF resignFirstResponder];
[self.thirdTF resignFirstResponder];
[self.fourthTF resignFirstResponder];
NSLog(#"Touches Work ");
return NO;
}
return YES;
}
Ensure that you have implemented this <UIGestureRecognizerDelegate> in your .h file.
This line ![touch.view isKindOfClass:[UITableViewCell class]] checks whether a tableViewCell was tapped and dismisses any active keyboard.
Solution for Swift, works in 2021. For this solution you don't have to have the reference(s) to the Table View(s).
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
//return !(touch.view is UITableViewCell) <-- doesn't work, the type of the touched class is not UITableViewCell anymore
var v = touch.view
while v != nil {
if v is UITableView { return false }
v = v?.superview
}
return true
}
Usage:
let gestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(YOUR_METHOD))
gestureRecognizer.delegate = self
YOUR_VIEW.addGestureRecognizer(gestureRecognizer)
Here is my solution, which ties the recognizer's shouldReceiveTouch directly to whether the keyboard is showing.
In your tap gesture recognizer delegate:
#pragma mark - UIGestureRecognizerDelegate
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch
{
if ([PFXKeyboardStateListener sharedInstance].visible) {
return YES;
}
return NO;
}
And the PFXKeyboardStateListener.h:
#interface PFXKeyboardStateListener : NSObject
{
BOOL _isVisible;
}
+ (PFXKeyboardStateListener *)sharedInstance;
#property (nonatomic, readonly, getter=isVisible) BOOL visible;
#end
And the PFXKeyboardStateListener.m:
static PFXKeyboardStateListener *sharedInstance;
#implementation PFXKeyboardStateListener
+ (PFXKeyboardStateListener *)sharedInstance
{
return sharedInstance;
}
+ (void)load
{
#autoreleasepool {
sharedInstance = [[self alloc] init];
}
}
- (void)dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
- (BOOL)isVisible
{
return _isVisible;
}
- (void)didShow
{
_isVisible = YES;
}
- (void)didHide
{
_isVisible = NO;
}
- (id)init
{
if ((self = [super init])) {
NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
[center addObserver:self selector:#selector(didShow) name:UIKeyboardDidShowNotification object:nil];
[center addObserver:self selector:#selector(didHide) name:UIKeyboardWillHideNotification object:nil];
}
return self;
}
#end
You may want to update the singleton pattern of the keyboard listener, I haven't gotten to it yet. Hope this works for everyone else as well as it works for me. ^^
Implement this method for delegate of UIGestureRecognizer:
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer
shouldReceiveTouch:(UITouch *)touch
{
UIView *superview = touch.view;
do {
superview = superview.superview;
if ([superview isKindOfClass:[UITableViewCell class]])
return NO;
} while (superview && ![superview isKindOfClass:[UITableView class]]);
return superview != nil;
}
My case was different including a uisearchbar and uitableview on self.view. I wanted to dismiss uisearchbar keyboard by touching on the view.
var tapGestureRecognizer:UITapGestureRecognizer?
override func viewWillAppear(animated: Bool) {
tapGestureRecognizer = UITapGestureRecognizer(target: self, action:Selector("handleTap:"))
}
On UISearchBar Delegate Methods:
func searchBarShouldBeginEditing(searchBar: UISearchBar) -> Bool {
view.addGestureRecognizer(tapGestureRecognizer!)
return true
}
func searchBarShouldEndEditing(searchBar: UISearchBar) -> Bool {
view.removeGestureRecognizer(tapGestureRecognizer!)
return true
}
When user touches on self.view:
func handleTap(recognizer: UITapGestureRecognizer) {
sampleSearchBar.endEditing(true)
sampleSearchBar.resignFirstResponder()
}
In swift you can use this inside
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
if CheckTheTime() == true {
// do something
}else{
}
}
func CheckTheTime() -> Bool{
return true
}
This is my solution based on above answers...
It's worked for me...
//Create tap gesture for menu transparent view
UITapGestureRecognizer *rightTableTransparentViewTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(rightTableTransparentViewTapMethod:)];
[rightTableTransparentViewTap setCancelsTouchesInView:NO];
[_rightTableTransparentView addGestureRecognizer:rightTableTransparentViewTap];
- (void)rightTableTransparentViewTapMethod:(UITapGestureRecognizer *)recognizer {
//Write your code here
}
ISSUE:
In my case, the issue was that I originally placed a button in each collectionView cell and set the constraints to fill the cell, so that when the cell was clicked it would click the button, however the buttons function was empty so nothing was appearing to be happening.
FIX:
I fixed this by removing the button from the collection view cell.