I have a UIScrollView that fills the screen on one page of my app, but I want to allow the user to pan from the edge of the screen to reveal a view behind it. The problem is that the UIScrollView steals the touches from my UIScreenEdgePanGestureRecognizer at the edge of the screen. Unfortunately, I can't access the UIScreenEdgePanGestureRecognizer from the view controller that has the UIScrollView (and vice-versa), so I am not able to use the method requireGestureRecognizerToFail: because I cannot able to specify which gesture recognizer should be allowed to fail. The view controller with the scroll view is a child view controller of a container view controller that has the screen edge pan gesture recognizer attached to one of the container view controller's own views.
I'm also unable to use the delegate method
-(BOOL)gestureRecognizer:shouldRequireFailureOfGestureRecognizer:
because the UIScrollView won't allow me to set my view controller as the delegate of the scroll view's UIPanGestureRecognizer.
How can I prevent the scrollview from stealing the edge pan touches from my own gesture recognizer?
Unfortunately, creating this behavior can be a real P*** i* t** A**.
Fortunately, creating this behavior is possible using the UIGestureRecognizer Delegate even if you can't access one GestureRecognizer directly.
-(BOOL)gestureRecognizer:shouldRequireFailureOfGestureRecognizer:
-(BOOL)gestureRecognizer:shouldRecognizeSimultaneouslyWithGestureRecognizer:
-(BOOL)gestureRecognizer:shouldBeRequiredToFailByGestureRecognizer:
The second parameter ('otherGestureRecognizer') passed in the delegate methods holds the UIScrollView's PanGestureRecognizer (or private Apple - subclasses) when your gestureRecognizer 'collides' with the scrollView's.
So simply set your UIScreenEdgePanGestureRecognizer's delegate to reply to the delegate methods.
The naming of these two methods is very suboptimal and to be honest, i don't really know what the correct return values for your case are.
I just had this problem yesterday and i solved it by brutal trial & error.
Im my case, returning NO from both shouldRequireToFail and shouldBeRequiredToFail methods and YES from the simultaneous-method solved my problem.
Note: Returning NO from both methods changed the behavior compared to not
even implementing the methods at all. Even though the documentation says
the default return value is NO.
However, ANY GestureRecognizer behavior can be achieved by using the delegate methods. But as i said above, the naming of the methods is just very confusing. + there is as much as NO useful documentation for these methods.
This can be done without having to set the screen pan gesture's delegate to your view controller.
[scrollView.panGestureRecognizer requireGestureRecognizerToFail:screenPanGesture];
Related
I have a container view controller managing its own full-screen content view, with several gesture recognizers attached. A child view controller can be overlaid over a portion of the screen; its root view is a UIView providing the opaque background color, which is covered by a UIScrollView, which in turn contains a complex view hierarchy of stack views, etc.
Scrolling in the child works correctly, as well as any user interactions with its subviews. The problem I'm having is that any taps or other non-scrolling gestures on the the scroll view itself (i.e. not inside any of its subviews) fall through the empty UIView behind it and are unexpectedly handled by the gesture recognizers on the root view of the parent (container) controller. I want those touches to be swallowed up by the child's background view so that they are ignored/cancelled.
My first thought was to override nextResponder on the child VC to return nil, assuming that would prevent touch events from passing to the superview. No success there, so I tried overriding the touch handling methods (touchesBegan: etc.) on the child controller, but they never get called. Then I substituted a simple UIView subclass to be the root view of my child controller, likewise trying both of those approaches there instead. Again returning nil for nextResponder has no effect, and the touch methods never get called.
My responder chain looks to be set up exactly as I would expect: scroll view --> child VC's root view --> child VC --> parent's root view --> parent VC. That makes me think my controller containment is set up correctly, and makes me suspect that the gesture recognizers on the parent's root view are somehow winning out over the responder chain in a way that I don't understand.
This seems like it should be easy. What am I missing? Thanks!
I think I understand better what's going on here thanks to this very helpful WWDC video.
Given an incoming touch, first the system associates that touch with the deepest hit-tested view; in my case that's the UIScrollView. Then it apparently walks back up the hierarchy of superviews looking for any other attached recognizers. This behavior is implied by this key bit of documentation:
A gesture recognizer operates on touches hit-tested to a specific view and all of that view’s subviews.
The scroll view has its own internal pan recognizer(s), which either cancel unrecognized touches or possibly fall back on responder methods that don't happen to forward touches up the responder chain. That explains why my responder methods never get called, even when my own recognizers are disabled.
Armed with this information, I can think of a few possible ways to solve my problem, such as:
Use gesture delegate methods to ignore touches if/when the associated view is under a child controller.
Write a "null" gesture recognizer subclass that captures all touches and ignores them, and attach that to the root view of the child controller.
But what I ended up doing was simply to rearrange my view hierarchy with a new empty view at the top, so that my child controller views can be siblings of the main content view rather than its subviews.
So the view hierarchy changes from this:
to this:
This solves my problem: my gesture recognizers no longer interact with touches that are hit-tested to the child controller's views. And I think it better captures the conceptual relationships between my app's controllers, without requiring any additional logic.
I want to implement a hold-to-preview button that brings up a view containing an AVPlayerLayer, which plays as long as the touch doesn't end. The video player is contained in a different view controller, and I am hoping to be able to use presentViewController:animated: when presenting it, and not just add it as a subview and child view controller.
My question is about how to deal with the touch event. I see two possible ways:
I try to transfer the active touch down event to the presented view controller (not sure if even possible), or
I try to keep the original view controller's gesture recognizer active, and then let the video view controller know when it's time to dismiss itself. I'm hoping this could be achieved either by just setting the presented view controller's userInteractionEnabled to false, or perhaps using a UIViewControllerTransitioningDelegate to present it, and then just skip calling completeTransition: or something similar (I believe touches don't register on the new view until you complete the animation, but please correct me if I'm wrong).
My question is about how to deal with the touch event.
Touches are always associated with the view that they start in. You can't transfer the touch to a different view. I've never tried it, but the options I think you should explore first are:
Use view controller containment. Make your preview view controller a child view controller of the one where the touch originates. That way the parent and its view hierarchy never go away, although they could be covered up.
Attach the gesture recognizer to the window. A window is a view, and should be able to have gesture recognizers. You could make the gesture recognizer's target the app delegate or some other object that will always be around, and have the delegate post a notification when the recognizer is triggered. Again, I haven't tried this, but it seems like it should work.
I have a UIPageViewController that displays images with Transition style scroll. I want to handle tap and pan gestures so in order to do that, I did a little hack of putting another view on top of the uipageviewcontroller and assign tap and pan gesture recognizers. In this case, i use -setViewControllers:direction:animated:completion: to perform swipes when i detect pan gestures to the left or right.
My problem is that when using -setViewControllers:direction:animated:completion: the datasource methods viewControllerBeforeViewController and viewControllerAfterViewController doesn't get called. Also the Delegate method -pageViewController:didFinishAnimating:previousViewControllers:transitionCompleted: doesnt get callled.
what do i have to do for these methods to be called using setViewControllers?
I think you can get UIPageController's gesture recognisers, and use them for disable or enable UIPageController interaction by using method of UIGestureRecogniser:
- (void)requireGestureRecognizerToFail:(UIGestureRecognizer *)otherGestureRecognizer
If you use setViewControllers... methods of dataSource will not called, because you've provided viewControllers. Also you can handle completion of appearing animation in the completion block of this method. (you can call methods of the dataSource and delegate by self)
UPD
When I had same task, I implemented custom page view (based on UIScrollView in pages state), and used gesture recognisers of the scrollView to avoid scrolling, while pan gesture is recognised inside some page. I can't remember why I've implemented custom control, maybe because I can't find solution for same problem. I remember that my page view used dataSource to get views for the pages. I think you can implement custom control with your special logic.
I'm trying to enable interactive pop gesture recogniser on my keyboard's accessory view. It does not work by default.
I passed an interactive pop gesture recogniser reference to my accessory view in order to forward its touch events to the recogniser
It particularly works: the navigation bar's title gets changed and the background of the accessory view reveals the previous view controller's view as if the transition did start. But the top view itself remains in place even if the gesture recogniser completes tracking.
I also tried to forward touch events to the navigation controller itself, to its view, to its top view controller and to their window. Nothing changed even after forwarding to all of them simultaneously
Any ideas what is missing?
It looks like it is not possible to reuse touch event instances in the responder chain. Once the sendEvent: on UIWindow is called, there is already a certain view owning the touch, so there is no cense in forwarding the UIEvent instances to other views or their gesture recognisers.
However, the owning view can forward events to its nextResponder()s (e.g.: one of the gesture recognisers attached to this view or to one of the subviews of the view)
The only chance to forward touches to another view (from another view hierarchy) or another view's gesture recognisers is before the UITouch object creation: i.e. on the UIWindow level during the hitTest:withEvent: method invocation, which calls the pointInside:withEvent: method
Anyway I'm not sure whether it is possible to forward touches from one UIWindow to another. Will update the answer later
http://www.russbishop.net/uitouchtypestylus?utm_campaign=iOS%2BDev%2BWeekly&utm_medium=email&utm_source=iOS_Dev_Weekly_Issue_225
http://smnh.me/hit-testing-in-ios/
I'm looking for help from someone who have used MMDrawerController.
I've downloaded example project and it has a table view as a center view controller. Pan gesture works correctly - it starts to open the drawer only if the gesture is mostly horizontal. Drawer does not open when you scroll table view. This is the desired behavior for me.
But when I set up my own project and there's scrollView or tableView - scrolling them up and down opens the drawer if the scrolling gesture has even the slightest horizontal component which is confusing and barely usable.
I've tried to understand what makes the difference in the example project looking through code, but with no success. I didn't find any gesture recognizer callback overrides or anything like that what is changing the gesture behavior.
I've looked through threads on SO considering MMDrawerController, but didn't find anything similar.
I do know that I can override some things in MMDrawerController subclass to change the gesture recognition completely and probably achieve desired behavior this way, but I don't want to reinvent the wheel here. Probably there is some easy answer that I overlooked.
I've found the origin of my problem. It has nothing to do with the MMDrawerController. In my project, I had a category which implements gestureRecognizer delegate's method which de facto overrides implementation in all ViewControllers. I should use subclass instead of a category in this case, this was a very bad design.