I would like to detect the (initial) touch position in my UIScrollView when the user starts dragging. I have googled this issue and many seem to struggle with this very issue. Now, while I still can't wrap my head around why Apple would not let users access touch information in a scroll view, I can't help but find a solution by myself. However all my tries failed, so I would like to ask you.
Here is what I thought would work:
I set up a UIPanGestureRecognizer like this in my UIScrollView subclass and add it to its gesture recognizers:
UIPanGestureRecognizer *tapDrag = [[UIPanGestureRecognizer alloc] initWithTarget:self action:#selector(touchedAndDragged:)];
tapDrag.cancelsTouchesInView = NO;
tapDrag.delegate = self;
[self addGestureRecognizer:tapDrag];
And the corresponding method:
-(void)touchedAndDragged:(UIPanGestureRecognizer*)t{
CGPoint loc = [t locationInView:self];
//do something with location (that is exactly what I need)
//...
//Now DISABLE and forward touches to scroll view, so that it scrolls normally
t.enabled = NO;
/****
?????
*****/
}
As indicated by the comments, I would like to disable the pan gesture after I have the point and then disable the recognizer(while STILL dragging!) and "pass" the touches to my scroll view, so that the user can scroll normally. Is that feasible at all? Is there any other solution to it ?
Well UIScrollView's already have a built in pan gesture that you could tap into. Usage would be as simple as setting your class as your scroll view's delegate (to utilize scrollViewWillBeginDragging) and using UIPanGestureRecognizer's -locationInView: to determine touch location.
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView
{
CGPoint location = [scrollView.panGestureRecognizer locationInView:scrollView];
NSLog(#"%#",NSStringFromCGPoint(location));
}
Why don't you grab the start location in -(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event that will be more convenient and efficient.
Related
I need to be able to detect immediate touch and get its position. (so didSelectRowAtIndexPath can't help us since it does not act immediately when scrolling up and down fast, you need to breathe in and select one by one)
Already tried everything I can think of. Touches began in each cell does not work because it suddenly behaves like didSelectRowIndexPath when implemented in custom cell class. Same result with TableViewController, the nature of touches began (you touch it, respond right away) just won't work.
* I'm not trying to TAP. Need to be able to get TOUCH (TapGesture does not respond when swiping very carefully/slowly but touches began always does) *
Not sure it's what you need, but you can create a TapGestureRecognizer.
You will likely run into conflicts with the UITableView's own gesture recognisers, but there are mechanisms to solve these which should hopefully let you achieve your desired behaviour (look up requireGestureRecognizerToFail and UIGestureRecognizerDelegate's gestureRecognizerShouldBegin and shouldRecognizeSimultaneouslyWithGestureRecognizer).
Try this.
In cellForIndexpath method.
UITapGestureRecognizer *singleFingerTap =
[[UITapGestureRecognizer alloc] initWithTarget:self
action:#selector(handleSingleTap:)];
singleFingerTap.delegate=self;
cell.contentView.tag=indexPath.row;
[cell.contentView addGestureRecognizer:singleFingerTap];
//The event handling method
- (void)handleSingleTap:(UITapGestureRecognizer *)recognizer {
NSLog(#"%ld",(long)recognizer.view.tag);
}
I think what you need to do, add UILongPressGestureRecognizer to tableview, so normal touch will scroll and long press will do whatever you want to. Set its minimumPressDuration like 0.2 so it won't take much time. Add action for UILongPressGestureRecognizer and in that method get location like:
CGPoint touchPointInView = [sender locationInView: self.view]; //location reespective to view
CGPoint touchPointInView1 = [sender locationInView: tableView]; //location respective to tableview
I am using iCarouselButtonDemo to create an arc button menu. I want to disable the scrolling when user touch the space other than the buttons. But now we can scroll the view by touching every point of the UIView. How can I detect the touch point of the view and disable the scrolling when user touch the outside of the 5 buttons
This is my view. This is scrolling when I touch even the bottom of the view. How can I stop it?
Thanks
In the iCarousel implementation file, you will add the following code in gestureRecognizerShouldBegin method. So it looks like this. It firstly get the touch point in the iCarousel view, and find the inner most view responding to the touch through hitTest. If the view is not a button, you stop the pan gesture.
if ([gesture isKindOfClass:[UIPanGestureRecognizer class]])
{
CGPoint point = [gesture locationInView:self];
UIView *touchedView = [self hitTest:point withEvent:nil];
if (![touchedView isKindOfClass:[UIButton class]]) {
return NO;
}
//ignore vertical swipes
i have a custom UITableViewCell with a DrawingView as a subview. if the user try to draw something on that view everytime the touch events are forwarded to the underlying ScrollView (UITableView) and than the view scrolls. How can i disable the forwarding from the touch/scroll-events to the scrollView, that the user can draw on the DrawingView?
Any idear's? I tests the exclusiveTouch property, methods like hitTest or touchBegan to captcher the events and stop the scrolling, but nothing helped. Thanks for helping!
The caveat here is that a 'drawing' motion could very easily be interpreted as a scrolling motion.
What you need to do is override pointInside on your cell.
Effectively:
- (BOOL)pointInside:(CGPoint)point
withEvent:(UIEvent *)event
{
if (CGRectContainsPoint(drawingView.frame, point)) {
// Use the point to do the drawing
[drawingView drawAtPoint:point];
// Disable scrolling for good measure
self.tableView.scrollView.scrollEnabled = NO;
return NO;
}
// Enable scrolling
self.tableView.scrollView.scrollEnabled = YES;
return [super pointInside:point withEvent:event];
}
What this means is that as long as the user touches your cell inside the drawingView, scrolling won't happen.
If you're looking to scroll and draw at the same time from within the drawingView, that's going to be a lot kludgier to pull off.
See if this works. You may have to do some extra work like forwarding the point to your drawingView to draw something at the point.
Be careful that even if your finger is touching the same point, the pointInside method could be called multiple times so take care of duplicate events being called.
In Xcode 5.1 I have created a simple test app for iPhone:
The structure is: scrollView -> contentView -> imageView -> image 1000 x 1000 on the top.
And on the bottom of the single view app I have seven draggable custom UIViews.
The dragging is implemented in Tile.m with touchesXXXX methods.
My problem is: once I add a draggable tile to the contentView in my ViewController.m file - I can not drag it anymore:
- (void) handleTileMoved:(NSNotification*)notification {
Tile* tile = (Tile*)notification.object;
//return;
if (tile.superview != _scrollView && CGRectIntersectsRect(tile.frame, _scrollView.frame)) {
[tile removeFromSuperview];
[_contentView addSubview:tile];
[_contentView bringSubviewToFront:tile];
}
}
The touchesBegan isn't called for the Tile anymore as if the scrollView would mask that event.
I've searched around and there was a suggestion to extend the UIScrollView class with the following method (in my custom GameBoard.m):
- (UIView*)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
UIView* result = [super hitTest:point withEvent:event];
NSLog(#"%s: %hhd", __PRETTY_FUNCTION__,
[result.superview isKindOfClass:[Tile class]]);
self.scrollEnabled = ![result.superview isKindOfClass:[Tile class]];
return result;
}
Unfortunately this doesn't help and prints 0 in debugger.
The problem is, partly, because user interactions are disabled on the content view. However, enabling user interactions disables scrolling as the view captures all touches. So here is the solution. Enable user interactions in storyboard, but subclass the content view like so:
#interface LNContentView : UIView
#end
#implementation LNContentView
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
UIView* result = [super hitTest:point withEvent:event];
return result == self ? nil : result;
}
#end
This way, hit test passes only if the accepting view is not self, the content view.
Here is my commit:
https://github.com/LeoNatan/ios-newbie
The reason Tile views don't get touches is that scroll view's pan gesture recogniser consumes the events. What you need is, attach a UIPanGestureRecongnizer to each of your tiles and configure them as follows:
UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc] initWithTarget:self action:#selector(pan:)]; // handle drag in pan:method
[tile addGestureRecognizer:pan];
UIPanGestureRecognizer *scrollPan = self.scrollView.panGestureRecognizer;
[scrollPan requireGestureRecognizerToFail:pan];
Here you let scroll view's pan gesture recogniser know that you only wish scrolling to happen if none of the tiles are bing dragged.
I've checked the approach — it does work indeed. Regarding your code, you'll need to handle all touches in the gesture recogniser rather than Tile view because touch events may be consumed/delayed by hit-tested view's gesture recogniser before they reach the view itself. Please refer to UIGestureRecognizer documentation to learn more about the topic.
It looks as ir one of the views in the hierarchy is capturing the events.
Have a look at the section
The Responder Chain Follows a Specific Delivery Path
Of the Apple doc's here
Edit:
Sorry I was writing from memory. This is how i resolved a similar issue in an app of myself:
I use UITapGestureRecognizer in the view(s) that I want to detect the touch. Implement the following delegate method of the UITapGestureRecognizer:
- (void) touchesBegan:(NSSet*)touches withEvent:(UIEvent*)event
The touches' set contains all the objects (views) that received the event.
I want both my UIScrollView and its subviews to receive all touch events inside the subview. Each can respond in its own way.
Alternatively, if tap gestures were forwarded to subviews, all would be well.
A lot of people are struggling in this general area. Here are a few of the many related questions:
How does UIScrollView steal touches from its subviews
How to steal touches from UIScrollView?
How to Cancel Scrolling in UIScrollView
Incidentally, if I override hitTest:withEvent: in the scroll view, I do see the touches as long as userInteractionEnabled is YES. But that doesn't really solve my problem, because:
1) At that point, I don't know if it's a tap or not.
2) Sometimes I need to set userInteractionEnabled to NO.
EDIT: To clarify, yes, I want to treat taps differently from pans. Taps should be handled by subviews. Pans can be handled by the scroll view in the usual way.
First, a disclaimer. If you set userInteractionEnabled to NO on the UIScrollView, no touch events will be passed to the subviews. So far as I'm aware, there's no way around that with one exception: intercept touch events on the superview of the UIScrollView, and specifically pass those events to the subviews of UIScrollView. To be honest, though, I don't know why you would want to do this. If you're wanting to disable specific UIScrollView functionality (like...well, scrolling) you can do that easily enough without disabling UserInteraction.
If I understand your question, you need tap events to be processed by the UIScrollView and passed to the subviews? In any case (whatever the gesture is), I think what you're looking for is the protocol method gestureRecognizer:shouldRecognizeSimultaneouslyWithGestureRecognizer: in the protocol UIGestureRecognizerDelegate. In your subviews, whatever gesture recognizers you have, set a delegate (probably whatever class is setting the UIGestureReconginzer in the first place) on the gesture recognizer. Override the above method and return YES. Now, this gesture will be recognized along with any other recognizers that might have 'stolen' the gesture (in your case, a tap). Using this method you can even fine tune your code to only send certain kinds of gestures to the subviews or send the gesture only in certain situations. It gives you a lot of control. Just be sure to read about the method, especially this part:
This method is called when recognition of a gesture by
either gestureRecognizer or otherGestureRecognizer would block the
other gesture recognizer from recognizing its gesture. Note that
returning YES is guaranteed to allow simultaneous recognition;
returning NO, on the other hand, is not guaranteed to prevent
simultaneous recognition because the other gesture recognizer's
delegate may return YES.
Of course, there's a caveat: This only applies to gesture recognizers. So you may still have problems if you're trying to use touchesBegan:, touchesEnded, etc to process the touches. You can, of course, use hitTest: to send raw touch events on to the subviews, but why? Why process the events using those methods in UIView, when you can attach a UIGestureRecognizer to a view and get all of that functionality for free? If you need touches processed in a way that no standard UIGestureRecognizer can provide, subclass UIGestureRecognizer and process the touches there. That way you get all the the functionality of a UIGestureRecognizer along with your own custom touch processing. I really think Apple intended for UIGestureRecognizer to replace most (if not all) of the custom touch processing code that developers use on UIView. It allows for code-reuse and it's a lot easier to deal with when mitigating what code processes what touch event.
I don't know if this can help you, but I had a similar problem, where I wanted the scrollview to handle double-tap, but forward single tap to subviews. Here is the code used in a CustomScrollView
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
UITouch* touch = [touches anyObject];
// Coordinates
CGPoint point = [touch locationInView:[self.subviews objectAtIndex:0]];
// One tap, forward
if(touch.tapCount == 1){
// for each subview
for(UIView* overlayView in self.subviews){
// Forward to my subclasss only
if([overlayView isKindOfClass:[OverlayView class]]){
// translate coordinate
CGPoint newPoint = [touch locationInView:overlayView];
//NSLog(#"%#",NSStringFromCGPoint(newPoint));
BOOL isInside = [overlayView pointInside:newPoint withEvent:event];
//if subview is hit
if(isInside){
Forwarding
[overlayView touchesEnded:touches withEvent:event];
break;
}
}
}
}
// double tap : handle zoom
else if(touch.tapCount == 2){
if(self.zoomScale == self.maximumZoomScale){
[self setZoomScale:[self minimumZoomScale] animated:YES];
} else {
CGRect zoomRect = [self zoomRectForScrollView:self withScale:self.maximumZoomScale withCenter:point];
[self zoomToRect:zoomRect animated:YES];
}
[self setNeedsDisplay];
}
}
Of course, the effective code should be changed, but at this point you should have all the informations you need to decide if you have to forward the event. You might need to implement this in another method as touchesMoved:withEvent:.
Hope this can help.
I was having this same problem, but with a scrollview that was inside UIPageViewController, so it had to be handled slightly differently.
By changing the cancelsTouchesInView property to false for each recognizer on the UIScrollView I was able to receives touches to buttons inside the UIPageViewController.
I did so by adding this code into viewDidLoad:
guard let recognizers = self.pageViewController.view.subviews[0].gestureRecognizers else {
print("No gesture recognizers on scrollview.")
return
}
for recognizer in recognizers {
recognizer.cancelsTouchesInView = false
}
If what you need is to differ between a touch and a scroll then you can test if touches has been moved. If this is a tap then touchHasBeenMoved will not be called then you can assume this is a touch.
At this point you can set a boolean to indicate if a movnent accoured and set this Boolean as a condition in your other methods.
I am on the road but if that's what you need I will be able to explain better later.
A hackish way to achieve your objective - not 100% exact - is to subclass the UIWindow and override the - (void)sendEvent:(UIEvent *)event;
A quick example:
in SecondResponderWindow.h header
//SecondResponderWindow.h
#protocol SecondResponderWindowDelegate
- (void)userTouchBegan:(id)tapPoint onView:(UIView*)aView;
- (void)userTouchMoved:(id)tapPoint onView:(UIView*)aView;
- (void)userTouchEnded:(id)tapPoint onView:(UIView*)aView;
#end
#interface SecondResponderWindow : UIWindow
#property (nonatomic, retain) UIView *viewToObserve;
#property (nonatomic, assign) id <SecondResponderWindowDelegate> controllerThatObserves;
#end
in SecondResponderWindow.m
//SecondResponderWindow.m
- (void)forwardTouchBegan:(id)touch onView:(UIView*)aView {
[controllerThatObserves userTouchBegan:touch onView:aView];
}
- (void)forwardTouchMoved:(id)touch onView:(UIView*)aView {
[controllerThatObserves userTouchMoved:touch onView:aView];
}
- (void)forwardTouchEnded:(id)touch onView:(UIView*)aView {
[controllerThatObserves userTouchEnded:touch onView:aView];
}
- (void)sendEvent:(UIEvent *)event {
[super sendEvent:event];
if (viewToObserve == nil || controllerThatObserves == nil) return;
NSSet *touches = [event allTouches];
UITouch *touch = [touches anyObject];
if ([touch.view isDescendantOfView:viewToObserve] == NO) return;
CGPoint tapPoint = [touch locationInView:viewToObserve];
NSValue *pointValue = [NSValue valueWithCGPoint:tapPoint];
if (touch.phase == UITouchPhaseBegan)
[self forwardTouchBegan:pointValue onView:touch.view];
else if (touch.phase == UITouchPhaseMoved)
[self forwardTouchMoved:pointValue onView:touch.view];
else if (touch.phase == UITouchPhaseEnded)
[self forwardTouchEnded:pointValue onView:touch.view];
else if (touch.phase == UITouchPhaseCancelled)
[self forwardTouchEnded:pointValue onView:touch.view];
}
It's not 100% conforms to what your were expecting - because your second responder view does not handle the touch event natively via -touchDidBegin: or so, and has to implement the SecondResponderWindowDelegate. However this hack does allow you to handle touch events on additional responders.
This method is inspired by and extended from MITHIN KUMAR's TapDetectingWindow