iOS: how to allow all gestures/events *with a couple exceptions* to pass through a top level view to its subviews - iphone

I have an atypical iOS interface. Perhaps it's not practical but I'm giving it a go. Hope someone can help!
I have a menu in the form of a UIVIew. It contains 5 small UIImageViews. A UIPinchGestureRecognizer is attached to the UIVIew. When pinched inward, the 5 UIImageViews animate from off screen to form a circle in the middle of the window. When pinched outward, they animate back offscreen. Everything works great there.
I'd like to be able to, at any point in the application, pinch the screen to reveal the menu, select one of the 'buttons' (UIImageView), and load the associated subview.
The real problem is, if the current visible view is a UIScrollView or UITableView, my app is having trouble figuring out whether the menu or other subview should handle the touch event. If I really focus and make sure two finger touch the screen at the EXACT same time, the pinch will work and pull the menu inward. But otherwise, it attempts to scroll the current visible view.
I would like all events except the pinch gesture, (and a tap gesture when the menu is visible), to pass through the menu view to the rest of the subviews.
I understand I can override the hitTest:withEvent method to determine the correct view to handle the event, but I'm unclear at this point how exactly to use it. Neither the Apple docs nor any answers I've read on stack overflow have made this method clear to me.
Any help is much appreciated.

As UITableView is a subclass of UIScrollView, it inherits all of UIScrollView's properties including its gesture recognisers.
UIScrollView declares a UIPinchGestureRecognizer and UIPanGestureRecognizer. I'm not sure of the implementation details but I imagine the UITableView disables the pinch gesture recogniser as you are not supposed to be able to zoom a tableview!
In any case, you can attach your own UIPinchGestureRecognizer to the table view:
UIPinchGestureRecognizer *yPGR = [[UIPinchGestureRecognizer alloc]
initWithTarget:probablySelf action:yourMenuShowSelectorHere];
UITableView *tv = ...
// ...
[tv addGestureRecognizer:yPGR];
Then, you can make sure that the UITableView scoll does NOT scroll until your pinch has failed:
[tv.panGestureRecognizer requireGestureRecognizerToFail:yPGR];
This way, the UITableView will not scroll until it is sure that it has not detected a pinch.
EDIT: UIScrollView only uses (or at least declares public access to) UIGestureRecognizers in iOS 5 and up.

Related

UIScrollView and content with requires touch move events

I have a view which is using single touch events (single finger) for drawing (lines, cycles, text, and so on).
Now I want to put this view inside of UIScrollView, which will allow zooming and panning. Of course two fingers are required to perform both zooming and panning.
What is the pattern do do that? I've found only examples when contents of UIScrollView accepts only single clicks (it contains only a buttons). Nothing what to do when contents require also touch moves.
Access the panGestureRecognizer property on the UIScrollView, and set its minimumNumberOfTouches property to two:
myScrollView.panGestureRecognizer.minimumNumberOfTouchesRequired = 2;
I'm thinking you should begin with scrolling disabled for scrollview and user interaction enabled for both. Then in touchesBegan: you should check for number of touch points. If it's only one (aka user wants to draw) you do nothing (disabling interaction for scrollview would disable it for all subviews as well). However, if the number of touch points is greater than one, enable scrolling then do
[UIView setUserInteractionEnabled: NO]
This way, no lines should be drawn on the UIView when you pinch or zoom with two fingers on the UIScrollView.
I helped developed an app that required a signature plate with in a scroll view, and a scroll view's subviews are weird to touch events, sometimes you have to hold your finger there for it to pass through the scroll view and reach the inner views, so there was a bit of a lag... but what i ended up doing was subclassing a UIScrollView and overriding the
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {}
method and if the touch landed with in the frame of the signature plate it would
[scrollView setScrollEnabled:NO]; and the drawing happened sooner and more smoother... the only problem is they couldn't scroll the scroll view from that box.. basically i created a dead spot with in the scroll view
but, that's a little wonky, i think a UITableView might work better for you... just make it 1 giant table cell, i think you will get better results....
Don't forget to enable 'Multiple Touch' for these features.

How to pass a 'tap' to UIButton that is underneath UIView with UISwipeGestureRecognizer?

I have a UIButton underneath a (transparent) UIView. The UIView above has a UISwipeGestureRecognizer added to it, and that is its only purpose - to detect certain swipe gestures. I want all other touches to be ignored by that UIView, and passed to other views (such as my UIButton underneath). Currently, the UIView above seems to be detecting the tap (for example), doing nothing (as it should be), and not letting the UIButton underneath get a chance to respond.
I would prefer not to implement my own swipe recognizer, if possible. Any solutions / advice? I basically just want to know how to tell a UIView to pay attention to only a certain type of added gesture recognizer, and ignore (and thus let through to views behind) all other touches.
Have you set:
mySwipeGesture.cancelsTouchesInView = NO;
to allow the touches to be sent to the view hierarchy as well as the gesture?
Additionally, ensure that the view on top is:
theTransparentView.opaque = NO;
theTransparentView.userInteractionEnabled = YES;
I've had pretty good success attaching gestures to the parent view without needing to create a transparent subview on top for the gesture. Are you sure you need to do that?
I must have just been in a funk yesterday - I woke up with a simple solution today. Add the UISwipeGesture to a view which is a superview to both the UIView and the UIButton. Then, when processing those swipes, figure out where the swipe originated, and whether that point is in the frame of where I used to have the UIView. (As I mentioned, the only reason for the existence of the UIView was to define a target area for these swipe gestures.)
Can't you put your button on top of the view and add gesture recognisers to that button too?
In the end, your UIButton inherits form UIView via UIControl. Therefore there is practically nothing that you could do with a view but not with a button.
In my case, I fixed it by not using a button, but rather a UITapGestureRecognizer. My pan gesture recognizer was added to the background view, and the tap gesture was added to a view above it.

Why does touchesBegan stop working when UIImageView in placed inside a UIScrollView?

UIView -> UIImageView
I know I have things somewhat working ok since I can tap on my UIImageView and see an NSLog() statement in my touchesBegan method.
.
UIView -> UIScrollView -> UIImageView
I drag that same UIImageView into a UIScrollView and touchesBegan no longer gets called when I tap on my UIImageView. (I haven't changed anything else. All the same connections, methods, and code remains unchanged.)
Why does touchesBegan no longer work? And what can I do to get it working again?
Add uitapgesture to get event
Code is
UITapGestureRecognizer *ges11=[[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(Handeltap:)];
[imagename addGestureRecognizer:ges11];
Create one action name "Handeltap" U will get called there.
by default UIImageView don't handle user gestures.
set UIImageView instance's userInteractionEnabled to YES
Have a look at the documentation for UIScrollView.
Because a scroll view has no scroll bars, it must know whether a touch signals an intent to scroll versus an intent to track a subview in the content. To make this determination, it temporarily intercepts a touch-down event by starting a timer and, before the timer fires, seeing if the touching finger makes any movement. If the timer fires without a significant change in position, the scroll view sends tracking events to the touched subview of the content view. If the user then drags their finger far enough before the timer elapses, the scroll view cancels any tracking in the subview and performs the scrolling itself. Subclasses can override the touchesShouldBegin:withEvent:inContentView:, pagingEnabled, and touchesShouldCancelInContentView: methods (which are called by the scroll view) to affect how the scroll view handles scrolling gestures.
I'd also recommend reading the Scroll View Programming Guide.

How does UIScrollView steal touches from its subviews?

I'd like to be able to create my own container subclasses of UIView which can react first to touches, before their subviews. This is tricky because normally a subview receives touch events (via touchesBegan: etc) before superviews. How does UIScrollView reverse this?
To be clear, I am not asking how UIScrollView behaves. I understand what it does, and how you would normally use it. I'm asking about how I could cleanly implement my own version of this -- not because I want to, but because I'm trying to build reusable container views that take advantage of similar behavior.
You can implement hitTest:withEvent: method in your UIView subclass.
This method gets called to check what subview must receive the touch event, so you can perform some action there before it actually happens. You can also change subview that must receive event.
[Edit: Okay, I somehow read the question as the exact opposite what was actually asked. I'll leave this here in case someone is curious but it's not relevant to the actual question -- TechZen]
A scroll view has move both horizontally and vertically so it has to sample all touches to see if they are scrolling touches.
For example, you have a slider in scrollview that scrolls horizontally. The user touches the slider. Is the touch intended to slide the slider or is it the start of a horizontal scroll? Which should have precedence?
Even though programmatically, the responder chain moves from the bottom most view (top most visually) up the chain until it finds an object that deals with it, behind the scenes the processing of a touch's location goes from the top most view (which is the app window) downward. Most views simply forward this information to subviews who then activate then send an event up the responder chain to be handled. Because scrolling the entire view is a special event that doesn't really belong in the ordinary responder chain, a scrollview intercepts the down pass of the touch's location in order to determine if it needs to scroll before it passes the touch locations to subviews.
Here is a very simply way, yet effective
UITapGestureRecognizer *gestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(hideKeyBoard:)];
gestureRecognizer.delegate = self;
[scrollView addGestureRecognizer:gestureRecognizer];
-(void) hideKeyBoard:(id) sender
{
// Do whatever such as hiding the keyboard
}

How can a superview interecept a touch sequence before any of its subviews?

I have a view hierarchy that is layed out as follows:
parentView
scrollView
contentViewA
containerView
contentViewB
contentViewC
I want contentViewB to respond to touches. Unfortunately scrollView makes this almost impossible because it tries to ingest the touches itself making touch response of contentViewB spotty.
So, instead, I want to intercept all touches in the parentView, manipulate contentViewB directly, and then pass the touches on to scrollView so it can do its thing.
Can someone please show me the correct way to pull this off?
Thanks in advance.
Cheers,
Doug
UPDATE:
I did a bit more digging and found the property canCancelContentTouches which seems to work wonders. I'm using IB so I unchecked "Cancellable Content Touches" in IB - first tab of the Scroll View Attribute Inspector. Now, when I run the app, touches appear to be arriving at contentViewB reliably.
Here's how the UIScrollView docs describe this property:
Discussion
If the value of this property is YES and a view in the content has begun tracking a finger touching it, and if the user drags the finger enough to initiate a scroll, the view receives a touchesCancelled:withEvent: message and the scroll view handles the touch as a scroll. If the value of this property is NO, the scroll view does not scroll regardless of finger movement once the content view starts tracking.
Rather opaque huh? Anyway, it seems to work.
To stop the scroll view from intercepting the touch events, set the userInteractionEnabled property like so:
scrollView.userInteractionEnabled = NO;
Another way of doing this is to add another subview to your ui so it looks like so :
parentView
scrollView
contentViewA
containerView
contentViewB
contentViewC
touchGrabber
and, in touchGrabber, detect all the touches that you want (by subclassing UIView)
This is more complicated than Phil Nash's solution but has the advantage that you can add/remove other views from your parentView without having to deal with their userInteractionEnabled value - this is useful if you have a third party library adding views for example.
However, if you definately only going to have the scrollView, Phil Nash's answer is the way forward!
Thanks,
Sam