I have a UIScrollView with a child UIView. I've gone ahead and added a UITapGestureRecognizer to the child view like so:
// in child
- (void)setup
{
[self addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(handleSingleTap:)]];
}
// and the handler
- (void)handleSingleTap:(UITapGestureRecognizer*)sender
{
if (sender.state == UIGestureRecognizerStateEnded)
NSLog(#"tap ended"); // this gets called
else if (sender.state == UIGestureRecognizerStateBegan)
NSLog(#"tap began"); // never get's called
}
This code could be in a view controller but I have reason to handle the touch in the view itself.
As you can see, I'm testing for the state of the tap. However the only state that get's called is when the user removes his finger from the screen, completing the tap. That is, UIGestureRecognizerStateEnded.
When the user taps on a child view (within the scrollView), I understand that the parent scroll view 'intercepts' all touches and decides if they are relevant (for panning, zooming, whatever...) and if not passes the touch(es) down to its subviews. Therefore touchesBegan:withEvent: never gets called on the scrollview's subviews.
WHAT I WOULD LIKE is to detect a touch down event (that is, the instant the user places a finger on the screen), within the child view, regardless of whether the user then pans, scrolls, or taps, and handle that tap within the child. The scrollview's behaviour should be unchanged.
Perhaps there is a better approach sans gesture recognizers such as overriding hitTest:withEvent but my puny human brain can't seem to figure it out!
UPDATE/EXAMPLE:
If you take a look at a UITableView which is a scrollview containing cells. You'll notice that placing your finger on a cell immediately highlights it. Then the act of scrolling 'cancels' the tap. That is generally the idea.
Adding the gesture recognizer to the child view puts you at the mercy of the scroll view, and the events it decides to pass down after it has determined that the user does not intend to scroll. If it fits your needs, one easy fix might be to set:
tableView.delaysContentTouches = NO;
This will cause touch methods to be called immediately.
http://developer.apple.com/library/ios/#documentation/uikit/reference/UIScrollView_Class/Reference/UIScrollView.html
Your other option is to listen for gestures on the scroll view itself, and use hit testing to determine how the user is interacting with child views.
Related
I'm using a tap gesture recognizer to position a little subview with a label within another UIView. When the user taps on a view, the label is populated with the title of the component that the user tapped on, and the subview is centered at the tap location.
I need to make sure that the initial position of my gesture recognizer matches the center of the subview as defined in the storyboard(before the user taps on a view), but it seems that I'm unable to find a way to pass this point over to the gesture recognizer.
Is there a way to initialize my tap gesture recognizer with a certain point within a view?
I'm not quite sure what you are asking. Gesture Recognizes do not have a "starting point". They receive various touch types for touches inside of a given view and allow you to uniquely handle each one.
If you want to simulate a touch on loading (which sort of sounds like that might be what you are looking to do), reorganize your code to be something like this:
- (void)viewDidLoad
{
//simulate touch here
[self touchedAtLocation:CGPointMake(100, 100)];
}
//Your delegate method
- (void)handleTap:(UITapGestureRecogizer *)recognizer
{
[self touchedAtLocation:[recognizer locationInView:self.view]];
}
- (void)touchedAtLocation:(CGPoint)location
{
//perform action based on location of touch
}
In this example, you could start the position/data of your subview based on what it would be if there was a touch at 100,100.
Note: I left out code configuring your gesture recognizer because it sounds like you already have that part under control. If not, I can post with more code.
A couple things that might help:
Gesture recognizers can be attached to any view in the view hierarchy. So if you want some small sub-sub-view to recognize the tap, you can addGestureRecognizer to that view.
When the gesture is recognized, you can test the gesture's location (and other aspects of it's state) before deciding to do anything about it. For example, let's say you only want the gesture to work if the user taps a very small space inside of a view...
- (void)handleTap:(UITapGestureRecogizer *)recognizer {
// get the location relative to the subview to which this recognizer is attached
CGPoint location = [recognizer locationInView:recognizer.view];
// tiny rect to test, also in the recognizer's view's coordinates
CGRect someSmallerRect = CGRectInset(recognizer.view.bounds, 10, 10);
if (CGRectContainsPoint(someSmallerRect, location)) {
// do whatever the touch should do
}
// otherwise, it's like it never happened
}
I have a UIScrollView ontop of my UIViewController recreating an effect like in the Gowalla iPhone app when you're on a spot's page. Under my scroll view I have a button that I want to be able to perform it's action even when the scroll view's frame covers it up (where it's ontop of the button, the scroll view's clear). How would I do something like this? Is it possible? (it has to be, Gowalla [somehow] did it)
As for me, I will transfer touch event to another view by override following methods.
– touchesBegan:withEvent:
– touchesMoved:withEvent:
– touchesEnded:withEvent:
– touchesCancelled:withEvent:
Like this way,
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
// pass touch event by default implement.
[super touchesBegan:touches withEvent:event];
// redirect the touch events.
[anotherView touchesBegain:touches withEvent:event];
}
hitTest:withEvent: is used to decide which view should response touch event on the view hierarchy tree. If the view doesn't want to response touch event, it will return nil in hitTest. As result the above touch event methods won't be called.
I have no idea what this "Gowalla" app might do; hopefully, your description of "a button behind the scroll view, that you can touch 'through' the scroll view" is accurate.
If your button behind the scroll view can be sized to fill the entire contentSize area of the scroll view without screwing up your interface, the easiest solution would be to just put it inside the scroll view (under all the other views) and do just that.
Otherwise, your best bet is probably to create a custom view with a clear background to be placed in the scroll view as above. The easy solution is to have the custom view (probably a UIControl) just do whatever touching the button does. If that's not possible for some reason, your best option would be to override hitTest:withEvent: on the custom view to return the underlying button. I'd be wary of overriding hitTest:withEvent: on the scroll view itself, as that might interfere with scrolling.
Try adding the button on top of the scroll view. The only problem with that is if you hit the button, you will not be able to interact with the scrollView, but it will still be visible.
I'm using the touchesEnded: method to do some work when I lift a finger off my UIScrollView, but my problem (and i've confirmed using NSLog) is that the touchesEnded: method seems to only get called when I tap on my scroll view and not when I touch and hold/slide my finger and then let go?
Is there another method I need to use? (btw i'm calling super as well)
I need a way to do stuff as soon as the user removes their fingers off the view
When you simply tap, the scroll view will pass touches through to its subviews. But if you start dragging, the scrollview will send a touchesCancelled message to the subview and process the touches itself. Check out the methods on UIScrollViewDelegate - there's probably something there you can use.
Alternatively, UIScrollView has a property canCancelContentTouches. If you turn that off, its subviews will always receive touches, but of course then the scroll view won't scroll.
When I add a new view after detecting user's long press, I get touchesCancelled event.
However, I want to preserve the long press event to newly added view.
What I want to implement is user touch & hold the screen, then new view added, and user can move touch around in the newly added view without touch up and touch down again.
But, when new view is added, I get touch cancel event, so the added view can not receive any touch event even user's touch is moving.
I'm using UILongPressGestureRecognizer to detect user's long press.
below is log message.
MyView touchesBegan x:524 y:854
MyView handleLongPress (LongPress Detected)
NewView added
MyView touchesCancelled x:526 y:854
and nothing happend...
what I'm expecting is...
MyView touchesBegan x:524 y:854
MyView handleLongPress (LongPress Detected)
NewView added
MyView touchesCancelled x:526 y:854
NewView touchBegan
NewView touchMoved
NewView touchMoved
NewView touchMoved
NewView touchMoved
...
Is there any solution?
Thanks in advance.
This is a tricky one - my idea for a solution is a little bit hacky, but I think it will work.
Add a transparent view over the entire area and this is the view that you add your long-press gesture recognizer. I will call this the interceptor view and the one behind will be called the visible view. When you detect your long press in the interceptor view you can add the new view to the visible view without interfering with the touches on the interceptor view, therefore you can track them and move the new view around on the visible view.
If you need to detect other touches, for example in buttons and other UI elements that are in the visible view, then you should create a subclass of UIView (InterceptorView) for your interceptor view and override hitTest:withEvent: as follows:
- (UIView*)hitTest:(CGPoint)point withEvent:(UIEvent*)event
{
// Notes:
// visibleView is a property of your InterceptorView class
// which should be set to the visible view when the interceptor
// view is created and added over the top of the visible view.
// See if there are any views in the visible view that should receive touches...
// Since the frame of the interceptor view should be the same as the frame of the
// visible view then the point doesn't need coordinate conversion.
UIView* passThroughView = [self.visibleView hitTest:point withEvent:event];
if(passThroughView == nil)
{
// The visible view and its sub-views don't want to receive this touch
// which means it is safe for me to intercept it.
return self;
}
// The visible view wants this touch, so tell the system I don't want it.
return nil;
}
This will mean that your interceptor view will handle the long press, except when the press is over an interactive part of the visible view, in that case it will allow the touch to pass through to the visible view and its sub-views.
I haven't tested this, it's just an idea, so please let me know how you get on with it :)
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
}