Overriding a framework gesture in AVPlayerViewController - ios

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.

Related

Why does this UITapGestureRecognizer not recognize taps?

I'm having a bit of a problem with a UITapGestureRecognizer. I create it this way:
self.userInteractionEnabled = YES;
UITapGestureRecognizer *tapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(handleTap:)];
tapRecognizer.cancelsTouchesInView = NO;
tapRecognizer.delaysTouchesBegan = YES;
tapRecognizer.delegate = self;
tapRecognizer.numberOfTapsRequired = 1;
tapRecognizer.numberOfTouchesRequired = 1;
[self addGestureRecognizer:tapRecognizer];
In the header file I also include and implement the shouldReceiveTouch: method like so:
-(BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch {
return YES;
}
I've looked through countless threads of people with similar problems and can't seem to find a solution. I've heard that if the view you're adding the gesture recognizer has subviews with userInteractionEnabled set to YES, that could possibly interfere with the tap recognition, so I also include this:
for(UIView *subview in self.subviews) {
subview.userInteractionEnabled = NO;
}
Anyone know why the gesture recognizer doesn't work?
Edit:
Here are some details:
I'm adding the UITapGestureRecognizer to a UIView subclass.
I add the gesture recognizer in the subclass' initWithFrame: method.
I've verified that the gesture recognizer is being added by stepping through the portion of the code where it's actually added.
The view controller that contains this view does not have any gesture recognizers attached to it, but does implement touchesBegan, touchesMoved, and touchesEnded. However, according to this question, that the view controller implements those shouldn't affect the gesture recognition of the view.
Edit 2:
I've verified that there are no other views blocking the view with the gesture recognizer from receiving touches. I've also verified that the view is actually being added to the containing view controller's view. It seems like the problem's elsewhere.
Your set up should work, so I would guess that one of two things is happening. You may have forgotten to actually add this view to the ViewController's view, or there maybe another view placed on top of this view which is stealing the touches?
Another possibility causing the problem is that the view on which the UITapGestureRecognizer is added is not big enough, so when you tap on the screen, the touch point is not located in the view's bounds.
Also make sure the UIView is not transparent. Set the background color to e.g. black. This solved the problem for me.

Get UITapGestureRecognizer to recognize touches when they begin?

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

Homemade tap gesture recognizer for iOS

I'd like to code my own tap gesture recognizer, to detect the number of taps and number of touches (I don't want to use the iOS tap gesture recognizer because I want to extend it later in various other manners) ;
I tried the following : use the first motionBegin number of touches as the numberOfTouches of the tap, increment the numberOfTaps, and start the tap detection timer to detect the tap gesture if no new taps has been seen in a while
The problem is that one quickly realises that when doing a double-touch tap gesture, iOS either correctly detects one motionBegin with a double touch, or two quick one touch events. I guess a correct implementation should try to detect those quick one touch events that happen closely, but I'm wondering if there is a better way to implement the gesture recognizer.
Someone knows how the iOS tap gesture is implemented?
1. Add UIGestureRecognizerDelegate in your .h file. like
#interface finalScreenViewController : UIViewController <UIGestureRecognizerDelegate>
{
// do your stuff
}
2. Create a view in your viewDidLoad method (or any other method) you wanna to add the gesture in your .m file
ex
UIView * myView=[[UIView alloc]init];
myView.frame=CGRectMake(0,0.self.view.frame.size.width,self.view.frame.size.height);
[self.view addSubView: myView];
UITapGestureRecognizer *letterTapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(tapMethod:)];
letterTapRecognizer.numberOfTapsRequired = 1;
[myView addGestureRecognizer:letterTapRecognizer];
3. you can get view by
- (void) tapMethod:(UITapGestureRecognizer*)sender {
UIView *view = sender.view;
NSLog(#"%d", view.tag);//By tag, you can find out where you had tapped.
}

How to respond only to a single-tap gesture while letting objects behind respond to double-taps?

I'm displaying a document in a UIWebView. I want to place a hotspot over the document to trigger an action when it is tapped, but I also want to maintain the default UIWebView behavior of auto-zooming the document when it is double-tapped. I can't figure out how to respond to the single-taps while letting the UIWebView respond to the double-taps.
I first set up the hotspot as a transparent UIButton with an action, but double-tapping the hotspot resulted in the hotspot action being called twice. So I removed the action from the button and attached a single-tap gesture instead:
UITapGestureRecognizer *singleTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(singleTapAction:)];
singleTap.numberOfTapsRequired = 1;
singleTap.delegate = self;
[self.hotspot addGestureRecognizer:singleTap];
[singleTap release];
This works the same as the normal button action. But then I created a double-tap gesture, and configured it to block the single-tap gesture with requireGestureRecognizerToFail:
UITapGestureRecognizer *doubleTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(zoomWebView:)];
doubleTap.numberOfTapsRequired = 2;
doubleTap.delegate = self;
[self.hotspot addGestureRecognizer:doubleTap];
[doubleTap release];
UITapGestureRecognizer *singleTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(singleTapAction:)];
singleTap.numberOfTapsRequired = 1;
[singleTap requireGestureRecognizerToFail:doubleTap];
singleTap.delegate = self;
[self.hotspot addGestureRecognizer:singleTap];
[singleTap release];
- (void)zoomWebView:(UITapGestureRecognizer *)gesture {
NSLog(#"double tap");
}
With this setup, a single-tap on the hotspot calls singleTapAction and a double-tap on the hotspot calls zoomWebView (a custom method). This is good because singleTapAction is no longer called twice, but bad because the UIWebView no longer responds to the double-tap.
I tried forwarding the double-tap event from my doubleTap gesture to the UIWebView by subclassing UITapGestureRecognizer, overriding the touchesBegan and touchesEnded methods, and sending their arguments on to the corresponding methods of the UIWebView. When I did that, I could see that my subclass was receiving the events, but the UIWebView didn't respond to the forwarded events. This is to be expected because the Event Handling Guide for iOS says that we can only forward events to custom subclasses of UIView, not to UIKit framework objects.
Is there a way to prevent my single-tap gesture from responding to double-taps that doesn't divert the double-tap events? This seems like a basic requirement, but I can't see a straightforward way to do it. I read about and experimented with UIGestureRecognizer's touch delivery properties, but no combination of values stopped the single-tap gesture from consuming the double-tap gesture.
By the way, the relationship between the hotspot and the UIWebView in my view hierarchy is that of "cousins" -- they are subviews of two sibling views. If I add the gesture recognizers to hotspot view, the web view or their "grandparent" view, I get the same results.
Okay, I found a solution in two parts:
1) I had to add my gestures to a parent view of the UIWebView. Unmatched events don't travel through overlapping objects in a view from layer to layer as I was imagining. Instead, they travel through the hierarchy of views from child to parent. So as long as I was adding my double-tap gesture to a sibling or "cousin" view of the UIWebView, it was never going to proceed on to the web view. This means that I can't use buttons or views laid out in Interface Builder to determine multiple hotspot areas. Instead, I have to redirect all single-tap events to one method and then look at the touch positions to determine what action to trigger.
2) I had to add the gestureRecognizer:shouldRecognizeSimultaneouslyWithGestureRecognizer: method to my view controller (the delegate of my gestures) and return YES. This allows my single-tap gesture to respond even when I'm displaying HTML, text or image content in the UIWebView, which implements its own gesture for these content types. I learned this part from this answer.
With these changes, I understand the hierarchy of events to be:
Single-tap web view: my single-tap gesture on the web view's parent view responds; the web view's single-tap gesture also responds if applicable because simultaneous gestures are enabled
Double-tap web view: my double-tap gesture on the web view's parent view responds (but doesn't do anything); my single-tap gesture does not respond because it is configured to only respond if the double-tap fails; the web view's double-tap event also responds because it is part of the view hierarchy (it seems that the double-tap functionality of the web view does not use a gesture because otherwise it would have take precedence over my own gesture in my original setup)
With that explanation out of the way, here's some working code:
- (void)viewDidLoad {
UITapGestureRecognizer *doubleTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(doubleTapWebView:)];
doubleTap.numberOfTapsRequired = 2;
doubleTap.delegate = self;
[self.webViewParent addGestureRecognizer:doubleTap];
[doubleTap release];
UITapGestureRecognizer *singleTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(singleTapWebView:)];
singleTap.numberOfTapsRequired = 1;
[singleTap requireGestureRecognizerToFail:doubleTap];
singleTap.delegate = self;
[self.webViewParent addGestureRecognizer:singleTap];
[singleTap release];
}
- (void)doubleTapWebView:(UITapGestureRecognizer *)gesture {
NSLog(#"double-tap");
// nothing to do here
}
- (void)singleTapWebView:(UITapGestureRecognizer *)gesture {
NSLog(#"single-tap");
CGPoint touchLocation = [gesture locationInView:self.webViewParent];
float x = touchLocation.x;
float y = touchLocation.y;
CGRect frame = self.webViewParent.frame;
if (y < frame.size.height * .33) {
NSLog(#"top");
} else if (y > frame.size.height * .67) {
NSLog(#"bottom");
} else if (x < frame.size.width * .33) {
NSLog(#"left");
} else if (x > frame.size.width * .67) {
NSLog(#"right");
} else {
NSLog(#"center");
}
}
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
return YES;
}
Always been a problem with mouse clicks since event handling began.
It's impossible at the instance of a single click, for any software to determine that a double click is about to happen.
So, if you want to handle single clicking and double clicking, you'll have to do your own double click handling.
Time the instances of single click, and generate a double click event of your own.
Hopefully that can transfer to tapping in your context.

disable pinch recognizer when 1 finger is lifted on the screen

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.

Resources