I have added a pinch gesture recognizer to a scroll view, using it to close a modal view controller. I did it like so:
UIPinchGestureRecognizer *closePinch = [[UIPinchGestureRecognizer alloc] initWithTarget:self action:#selector(closeGallery)];
[galleryScrollView addGestureRecognizer:closePinch];
Although it is added to a scrollView, I do not actually use it to zoom only to close the view. Therefore, I have no need for the pinch-out gesture as it indicates zooming in.
Is there a way to easily disable the pinch-out portion of the gesture recognizer and leave the pinch-in untouched?
Based on Crazyrems' answer, the following delegate method did exactly what I needed:
- (BOOL)gestureRecognizerShouldBegin:(UIPinchGestureRecognizer *)gestureRecognizer
{
// Negative velocity indicates pinch out
if (gestureRecognizer.velocity < 0) {
return YES; // <- Register touch event
} else {
return NO; // <- Do not register touch event
}
}
You should implement -gestureRecognizerShouldBegin: in your UIGestureRecognizerDelegate
There's a velocity property in the recognizer passed in parameter, so you can check if it's a pinch in or out, and return YES or NO in consequence.
Related
I want the ability to change how the playback controls are presented while using the new AVPlayerViewController in AVKit. Basically, I want to overide the single finger tap gesture to do something else, and replace that gesture with a double finger tap. I am subclassing AVPlayerViewController to add this functionality.
I can add the double finger tap easily by creating a new UITapGestureRecognizer, but doing so with a single tap does nothing, as the playback controls still appear and my custom gesture method is not called. I assume because the AVPlayerViewController has a gesture with priority that is called instead.
I setup the gestures like normal...
// singleFingerTap: will never fire...
UITapGestureRecognizer *singleFingerTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(singleFingerTap:)];
singleFingerTap.numberOfTouchesRequired = 1;
singleFingerTap.delegate = self;
[self.view addGestureRecognizer:singleFingerTap];
// doubleFingerTap: will work correctly...
UITapGestureRecognizer *doubleFingerTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(doubleFingerTap:)];
doubleFingerTap.numberOfTouchesRequired = 2;
doubleFingerTap.delegate = self;
[self.view addGestureRecognizer:doubleFingerTap];
Any thoughts on how to achieve this without accessing private properties? Is it even permitted? I know I can create my own view controller with an instance of AVPlayer and then create my own playback controls, but I'm hoping I can use the lightweight AVKit player here with a few simple modifications.
I've tried looping through the gestures in AVPlayerViewController's view and removing them, but the gestureRecognizers property was empty. Even if I could do this, I wouldn't know how to add back the gesture to display the playback controls on a double finger tap instead of the single finger tap.
Any suggestions would be much appreciated! Especially whether this is possible/allowed or not. Thanks!
EDIT:
I have found a way to explicitly unblock the player's private gesture that was blocking my own gesture.
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldBeRequiredToFailByGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
id firstGesture = gestureRecognizer;
id secondGesture = otherGestureRecognizer;
if ([firstGesture isKindOfClass:[UITapGestureRecognizer class]] && [secondGesture isKindOfClass:[UITapGestureRecognizer class]]) {
UITapGestureRecognizer *tapGesture = firstGesture;
UITapGestureRecognizer *otherTapGesture = secondGesture;
if (tapGesture.numberOfTapsRequired == otherTapGesture.numberOfTapsRequired && tapGesture.numberOfTouches == otherTapGesture.numberOfTouches) {
// Disable the single tap that shows the playback controls...
return YES;
}
}
return NO;
}
This effectively prevents the playback controls from appearing on a tap, and my singleTapGesture: fires as expected. However, I now have the issue of getting the playback controls to appear on a different gesture. Is it possible to reroute the private gesture, or simulate the private gesture programmatically?
Why not just check/modify the target/remove the default gesture recognizers first?
You can access them with the standard UIView's gestureRecognizers.
The gestures recognizers may be buried inside a private subview.
Another solution would be to set userInteractionEnabled to NO and add your gesture recognizers to the superview or to a "overlay" transparent view.
I have two subviews inside the view. The frame is equal. A view has a swipe left gesture recognizer and B view has a swipe right gesture recognizer. B is behind A so I can only trigger swipe left gesture. How can I receive both?
The view in the back will not respond because, well, it's in the back.
For both gestures to respond simultaneously (left+right at the same time), add a UIGestureRecognizerDelegate to either and handle shouldRecognizeSimultaneouslyWithGestureRecognizer as suggested below.
If you want B to respond to a gesture, even though it is partially hidden by A, rethink your hierarchy: add a 3rd view above A & B (call it C), same size as B, as a placeholder to your gesture recognizer.
Your delegate:
// leftGesture
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer
shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer
*)otherGestureRecognizer
{
return otherGestureRecognizer == rightGesture;
}
How can I receive both?
Add both gesture recognizers to view A, since that's the view in front. Since you need the right swipe gesture to be handled by view B, make view B the target for the right swipe gesture recognizer.
A better option might be to make your view controller the target of both gesture recognizers and let it sort out what to do with the user's gestures. That helps to get the views out of the business of knowing how the user interface is set up -- they just have to follow instructions from the view controller. If you adopt that plan, your view controller might look like this in part:
- (void)viewDidLoad
{
UISwipeGestureController *leftSwipe = [[UISwipeGestureController alloc] initWithTarget:self action:#selector(leftSwipe:)];
leftSwipe.direction = UISwipeGestureRecognizerDirectionLeft;
UISwipeGestureController *rightSwipe = [[UISwipeGestureController alloc] initWithTarget:self action:#selector(rightSwipe:)];
rightSwipe.direction = UISwipeGestureRecognizerDirectionRight;
[self.viewA addGestureRecognizer:leftSwipe];
[self.viewA addGestureRecognizer:rightSwipe];
}
- (IBAction)leftSwipe:(id)sender
{
[self.viewA doSomething];
}
- (IBAction)rightSwipe:(id)sender
{
[self.viewB doSomethingElse];
}
Is there any way to get UITapGestureRecognizer to run on touch began?
I can't use touchesBegan because I am using a UITableView and the super view steals the event essentially.
I just want to detect when the screen is first touched. Why is this so difficult? Maybe I need a different solution than using tapgesturerecognizer?
You can use state property of UIGestureRecognizer to identify various states of any gesture -
#property(nonatomic,readonly) UIGestureRecognizerState state; // the current state of the gesture recognizer
So when the gesture begin, use something like this in your registered handler method -
if (gestureRecognizer.state == UIGestureRecognizerStateBegan) {
// Do your stuff
}
You can add a tap gesture recognizer to the tableView in viewDidLoad like this:
UITapGestureRecognizer *tapGestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(tapGestureRecognized:)];
[self.tableView addGestureRecognizer:tapGestureRecognizer];
Then implement this method:
- (void)tapGestureRecognized:(UITapGestureRecognizer *)tapGestureRecognizer {
NSLog(#"tap gesture recognized");
}
Just tested this out, and works fine. For every tap i get the log message on my console. Note that this prevents the tableview from receiving the taps, other gestures will just be handled by the table view as usual.
You need to set delaysContentTouches = NO
I want to achieve the following.
Scenario: The iOS keyboard is on-screen while the user is typing into a particular text field. The user can tap anywhere outside of the keyboard and text field to dismiss the keyboard (without activating any buttons which are visible). Also, the user can drag outside of the keyboard and observe the normal drag behavior on some arrangement of scrollable views.
Conceptually, I’m placing a “cover” UIView over most of the screen which behaves such that:
If the user taps on the cover, then I capture that tap (so that I can, e.g., dismiss the keyboard). This is easy to achieve by intercepting touch events in a UIView subclass or using a tap gesture recognizer.
If the user drags on the cover, then the cover ignores or forwards these touches; these are received by the layers underneath just as they would have been without a cover.
So: the user should be able to scroll content underneath the cover, but not tap content underneath the cover. A tap “outside” of the keyboard and text field should dismiss the keyboard (and cover), but should not activate anything.
How can I achieve this?
Add the tap gesture the usual way:
UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(tapAction:)];
[self.view addGestureRecognizer:tapGesture];
But what you may be looking for is this :
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
{
return YES;
}
Documentation says : This method is called when recognition of a gesture by either gestureRecognizer or otherGestureRecognizer would block the other gesture recognizer from recognizing its gesture. (https://developer.apple.com/library/ios/documentation/UIKit/Reference/UIGestureRecognizerDelegate_Protocol/index.html#//apple_ref/occ/intf/UIGestureRecognizerDelegate)
This way, you may be sure it's totally transparent, and also that nothing will prevent your recognizer from being called.
A custom view which forwards all touches it receives:
class CustomView: UIView {
override func hitTest(point: CGPoint, withEvent event: UIEvent?) -> UIView? {
var hitView = super.hitTest(point, withEvent: event)
if hitView == self {
return nil
}
return hitView
}
}
From there you can go different ways to just make use of tap gestures. Either observe the UIEvent for its touches, or use a gesture recognizer.
1: Add a tap gesture recognizer to the view:
//Adding tap gesture
UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(handleTapGesture:)];
tapGesture.numberOfTapsRequired = 1;
[self.view addGestureRecognizer:tapGesture];
2: In handleTapGesture you resignFirstResponder of the keyboard
- (void)handleTapGesture:(UITapGestureRecognizer *)sender {
if (sender.state == UIGestureRecognizerStateRecognized) {
//Resign first responder for keyboard here
}
}
Elaborated a bit on the answer above. UIGestureRecognizerStateRecognized makes sure it's single tab events that gets recognized.
Is this the functionality you where after?
I have a 2d map that the user can zoom and pan using the gesture recognizers. While it works, i want the user to start panning immediately after a zoom once they have 1 finger lifted. Unfortunately, in the docs it says:
The gesture ends (UIGestureRecognizerStateEnded) when both fingers
lift from the view.
which is pretending me from going from a pinch zoom to a pan right away. What could i do to fix this?
This is possible, and easy! It involves being your gesture recognizer's delegate. Something no-one seems to know exists. In my view controller subclass I have declared both conforming to the protocol <UIGestureRecognizerDelegate> and two ivars:
UIPinchGestureRecognizer *myPinchGR;
UIPanGestureRecognizer *myPanGR;
These ivars are instantiated in view did load. Notice setting self as the delegate.
-(void)viewDidLoad{
[super viewDidLoad];
myPanGR = [[UIPanGestureRecognizer alloc] initWithTarget:self action:#selector(panTarget:)];
myPanGR.delegate = self;
[self.view addGestureRecognizer:myPanGR];
myPinchGR = [[UIPinchGestureRecognizer alloc] initWithTarget:self action:#selector(pinchTarget:)];
myPinchGR.delegate = self;
[self.view addGestureRecognizer:myPinchGR];
}
One of the delegate calls made by a UIGestureRecognizer is shouldRecognizeSimultaneouslyWithGestureRecognizer: if I had more than two gesture recognizers then this function would have to contain some logic. But since there are only two I can just return YES.
-(BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer{
return YES;
}
Now you do have to include a little (very little) extra logic in your action methods to screen for the appropriate conditions.
-(void)panTarget:(UIPanGestureRecognizer *)panGR{
if (panGR.numberOfTouches > 1) return;
NSLog(#"panny");
}
-(void)pinchTarget:(UIPinchGestureRecognizer *)pinchGR{
if (pinchGR.numberOfTouches < 2) return;
NSLog(#"pinchy");
}
Run this code an look at the logs. you will see when you move one finger you will see "panny" when you place a second finger down you will see "pinchy", and back and forth.
Use this code inside the gesture handling method.
if (gesture.numberOfTouches != 2) {
// code here to end pinching
}
As Gesture handling method will be called immediately when user lift a finger while 2 finger pinching.