Touch events within ~8 pixels of nav bar not called - iphone

I have an application with three buttons (actually UIView's) laid out horizontally underneath the navigation bar.
The three buttons are of substantial enough size (larger than the back button, for example), however they won't respond to touches when the hit is in the top third part, roughly.
I'm aware that this area directly underneath the nav bar is sort of 'reserved' for the back button and other UINavigation items having a touch area that expands beyond the navigation bar (by quite a significant margin), however in some instance there isn't even a navigation item nearby to steal the event, and my views still don't respond.
The weird thing is that I am getting a call to the hitTest method in my UIView, just never a touchesBegan/Ended/etc.
The result is that it is very difficult to press the buttons and if one is anywhere near a UINavigationItem, the item will steal the event, even though in hitTest I am returning the correct UIView to the system.
Unfortunately I am the implementer and not the designer so a design change is a last resort.
Any ideas? Thanks!

You have to subclass your UINavigationBar and in your CustomNavigationBar do this:
-(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
if ([self pointInside:point withEvent:event]) {
self.userInteractionEnabled = YES;
} else {
self.userInteractionEnabled = NO;
}
return [super hitTest:point withEvent:event];
}
Info about how to subclass UINavigationBar you can find here.

The code provided by #Andrei will cause controller stack inconsistency on iOS 7 when quickly push and pop controllers. The system itself will change the userInteractionEnabled property before and after push/pop animations. Here is how I fixed this issue.
#interface DMNavigationBar ()
#property (nonatomic, assign) BOOL changingUserInteraction;
#property (nonatomic, assign) BOOL userInteractionChangedBySystem;
#end
#implementation DMNavigationBar
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
if (self.userInteractionChangedBySystem && self.userInteractionEnabled == NO)
{
return [super hitTest:point withEvent:event];
}
if ([self pointInside:point withEvent:event])
{
self.changingUserInteraction = YES;
self.userInteractionEnabled = YES;
self.changingUserInteraction = NO;
}
else
{
self.changingUserInteraction = YES;
self.userInteractionEnabled = NO;
self.changingUserInteraction = NO;
}
return [super hitTest:point withEvent:event];
}
- (void)setUserInteractionEnabled:(BOOL)userInteractionEnabled
{
if (!self.changingUserInteraction)
{
self.userInteractionChangedBySystem = YES;
}
else
{
self.userInteractionChangedBySystem = NO;
}
[super setUserInteractionEnabled:userInteractionEnabled];
}
#end

Had the same problem and the above answers are good and they point to a concise solution.
The same answer for the swift version:
class APLVNavigationBar: UINavigationBar {
override func hitTest(point: CGPoint, withEvent event: UIEvent?) -> UIView? {
if pointInside(point, withEvent: event){
userInteractionEnabled = true
}else{
userInteractionEnabled = false
}
return super.hitTest(point, withEvent: event)
}
}

I dont think that it is the problem, but did you bring the button subviews to the front?
[self.view bringSubviewToFront:buttonView]

Related

hit test to avoid subview but want another subviews to work

I have a background subview which just have grey color's, and got tiles on top of it which can be dragged. This is all happening in one view controller. The problem is that when I do a hit test and try to avoid the backgroundView, since the tiles are on top of that view, it still see's those coordinates and avoid the touches move event since I am returning nil.
Below is the code:
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
for (UIView *view in self.subviews) {
//This checks if touches are within the region of the custom view based on its cordinates.
if (CGRectContainsPoint(view.frame, point)) {
NSLog(#"touched the boat view %f %f and %f %f",view.frame.origin.x, view.frame.origin.y, point.x, point.y);
if ([view isEqual:backgroundView]) {
return nil;
}
else
return view;
}
}
return nil;
}
If you can see I am verifying for the background view, but since my tiles are on of the background view, those are not being dragged based on my other logic. I have tried userInteractiveEnabled to No, but that doesn't seams to work. Any suggestions?
you should have a subclass of uiview and implement hitTest on this subclass :
#implementation EDPassTroughView
- (id)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
// Initialization code
}
return self;
}
-(UIView*)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
UIView *hitView = [super hitTest:point withEvent:event];
if (hitView == self)
return nil;
else
return hitView;
}
#end

Passing through touches to UIViews underneath

I have a UIView with 4 buttons on it and another UIView on top of the buttons view. The top most view contains a UIImageView with a UITapGestureRecognizer on it.
The behavoir I am trying to create is that when the user taps the UIImageView it toggles between being small in the bottom right hand corner of the screen and animating to become larger. When it is large I want the buttons on the bottom view to be disabled and when it is small and in the bottom right hand corner I want the touches to be passed through to the buttons and for them to work as normal. I am almost there but I cannot get the touches to pass through to the buttons unless I disable the UserInteractions of the top view.
I have this in my initWithFrame: of the top view:
// Add a gesture recognizer to the image view
UITapGestureRecognizer *tapGestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(imageTapped:)];
tapGestureRecognizer.cancelsTouchesInView = NO;
[imageView addGestureRecognizer:tapGestureRecognizer];
[tapGestureRecognizer release];
and I this is my imageTapped: method:
- (void) imageTapped:(UITapGestureRecognizer *) gestureRecognizer {
// Toggle between expanding and contracting the image
if (expanded) {
[self contractAnimated:YES];
expanded = NO;
gestureRecognizer.cancelsTouchesInView = NO;
self.userInteractionEnabled = NO;
self.exclusiveTouch = NO;
}
else {
[self expandAnimated:YES];
expanded = YES;
gestureRecognizer.cancelsTouchesInView = NO;
self.userInteractionEnabled = YES;
self.exclusiveTouch = YES;
}
}
With the above code, when the image is large the buttons are inactive, when I touch the image it shrinks and the buttons become active. However, the small image doesn't receive the touches and therefore wont expand.
If I set self.userInteractionEnabled = YES in both cases, then the image expands and contracts when touched but the buttons never receive touches and act as though disabled.
Is there away to get the image to expand and contract when touched but for the buttons underneath to only receive touches if the image is in its contracted state? Am I doing something stupid here and missing something obvious?
I am going absolutely mad trying to get this to work so any help would be appreciated,
Dave
UPDATE:
For further testing I overrode the touchesBegan: and touchesCancelled: methods and called their super implementations on my view containing the UIImageView. With the code above, the touchesCancelled: is never called and the touchesBegan: is always called.
So it would appear that the view is getting the touches, they are just not passed to the view underneath.
UPDATE
Is this because of the way the responder chain works? My view hierarchy looks like this:
VC - View1
-View2
-imageView1 (has tapGestureRecogniser)
-imageView2
-View3
-button1
-button2
I think the OS first does a hitTest as says View2 is in front so should get all the touches and these are never passed on to View3 unless userInteractions is set to NO for View2, in which case the imageView1 is also prevented from receiving touches. Is this how it works and is there a way for View2 to pass through it's touches to View3?
The UIGestureRecognizer is a red herring I think. In the end to solve this I overrode the pointInside:withEvent: method of my UIView:
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
BOOL pointInside = NO;
if (CGRectContainsPoint(imageView.frame, point) || expanded) pointInside = YES;
return pointInside;
}
This causes the view to trap all touches if you touch either the imageView or if its expanded flag is set. If it is not expanded then only trap the touches if they are on the imageView.
By returning NO, the top level VC's View queries the rest of its view hierarchy looking for a hit.
Select your View in Storyboard or XIB and...
Or in Swift
view.isUserInteractionEnabled = false
Look into the UIGestureRecognizerDelegate Protocol. Specifically, gestureRecognizer:shouldReceiveTouch:
You'll want to make each UIGestureRecognizer a property of your UIViewController,
// .h
#property (nonatomic, strong) UITapGestureRecognizer *lowerTap;
// .m
#synthesize lowerTap;
// When you are adding the gesture recognizer to the image view
self.lowerTap = tapGestureRecognizer
Make sure you make your UIViewController a delegate,
[self.lowerTap setDelegate: self];
Then, you'd have something like this,
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch {
if (expanded && gestureRecognizer == self.lowerTap) {
return NO;
}
else {
return YES;
}
}
Of course, this isn't exact code. But this is the general pattern you'd want to follow.
I have a another solution. I have two views, let's call them CustomSubView that were overlapping and they should both receive the touches. So I have a view controller and a custom UIView class, lets call it ViewControllerView that I set in interface builder, then I added the two views that should receive the touches to that view.
So I intercepted the touches in ViewControllerView by overwriting hitTest:
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
return self;
}
Then I overwrote in ViewControllerView:
- (void)touchesBegan:(NSSet *)touches
withEvent:(UIEvent *)event
{
[super touchesBegan:touches withEvent:event];
for (UIView *subview in [self.subviews reverseObjectEnumerator])
{
if ([subview isKindOfClass:[CustomSubView class]])
{
[subview touchesBegan:touches withEvent:event];
}
}
}
Do the exact same with touchesMoved touchesEnded and touchesCancelled.
#Magic Bullet Dave's solution but in Swift
Swift 3
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
var pointInside = false
if commentTextField.frame.contains(point) {
pointInside = true
} else {
commentTextField.resignFirstResponder()
}
return pointInside
}
I use it in my CameraOverlayView for ImagePickerViewController.cameraOverlay to give user ability to comment while taking new photo

UIScrollView Horizontal scrolling - Disable left scroll

How can I disable scrolling left on a UIScrollview. This is so users can only scroll right?
Thanks
*** UPDATE *****
This is what I have now. I have subclassed UIScrollView and overwritten the following methods
#implementation TouchScrollView
- (BOOL)touchesShouldBegin:(NSSet *)touches withEvent:(UIEvent *)event inContentView:(UIView *)view {
NSLog(#"touchesShouldBegin: I was touched!");
return YES;
}
- (BOOL)touchesShouldCancelInContentView:(UIView *)view {
NSLog(#"touchesShouldCancelInContentView: I was touched!");
return NO;
}
In my viewController I have also set the following attributes:
scrollView.delaysContentTouches = NO;
scrollView.canCancelContentTouches = YES;
TouchesShouldBegin is being called but touchesShouldCancelInContentView is not being called and I cant figure out why!!
Thanks
just add this in your UIViewController UIScrollViewDelegate
float oldX; // here or better in .h interface
- (void)scrollViewDidScroll:(UIScrollView *)aScrollView
{
if (scrollView.contentOffset.x < oldX) {
[aScrollView setContentOffset: CGPointMake(oldX, aScrollView.contentOffset.y)];
} else {
oldX = aScrollView.contentOffset.x;
}
}
You should handle swipe gestures.
Check similar question here: iOS Advanced Gestures: Getting Swipe Direction Vector

How to detect touch on UIWebView

On UIWebview, how can I detect a touch?
But not when user clicks some URL or touching a control.
Is it possible to handle it?
Use UIGestureRecognizerDelegate method:
Add UIGestureRecognizerDelegate in declaration file (i.e. your .h file)
Step 1: Just set the delegate of gestureRecognizer: (in .m file viewDidLoad)
UITapGestureRecognizer *webViewTapped = [[UITapGestureRecognizer alloc]initWithTarget:self action:#selector(tapAction:)];
webViewTapped.numberOfTapsRequired = 1;
webViewTapped.delegate = self;
[offScreenWebView addGestureRecognizer:webViewTapped];
[webViewTapped release];
Step 2: Override this function: (in .m file)
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
{
return YES;
}
Step 3: Now implement the tapAction function:
- (void)tapAction:(UITapGestureRecognizer *)sender
{
NSLog(#"touched");
// Get the specific point that was touched
CGPoint point = [sender locationInView:self.view];
}
The accepted answer is great if you only need to detect taps. If you need to detect all touches, the best way is to create a new UIView subclass and place it over the webview. In the subclass you can detect touches using hitTest:
TouchOverlay.h
#class TouchOverlay;
#protocol TouchOverlayDelegate <NSObject>
#optional
- (void)touchOverlayTouched:(TV4TouchOverlay *)touchOverlay;
#end
#interface TouchOverlay : UIView
#property (nonatomic, unsafe_unretained) id <TouchOverlayDelegate> delegate;
#end
Touchoverlay.m
#implementation TouchOverlay
- (id)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
return self;
}
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
UIView *hitView = [super hitTest:point withEvent:event];
if (hitView == self) {
if (self.delegate && [self.delegate respondsToSelector:#selector(touchOverlayTouched:)]) {
[self.delegate touchOverlayTouched:self];
}
return nil; // Tell the OS to keep looking for a responder
}
return hitView;
}
#end
Note that the accepted answer above will only capture tap gestures (touchDown and touchUp without a drag in between), and that swipe gestures will be ignored.
For my purposes I needed to be informed of both, and so I added swipe gesture recognizers appropriately. (Note that despite being a bit field, you can't OR together swipe gesture recognizers' direction property, so 4 gesture recognizers are required to detect any swipe).
// Note that despite being a bit field, you can't `OR` together swipe gesture
// recognizers' `direction` property, so 4 gesture recognizers are required
// to detect any swipe
for (NSNumber * swipeDirection in #[#(UISwipeGestureRecognizerDirectionUp), #(UISwipeGestureRecognizerDirectionDown), #(UISwipeGestureRecognizerDirectionLeft), #(UISwipeGestureRecognizerDirectionRight)]) {
UISwipeGestureRecognizer * swipe = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:#selector(timerReset:)];
swipe.direction = [swipeDirection integerValue];
swipe.delegate = self;
[rootWebView addGestureRecognizer:swipe];
}
Everything that inherits from UIResponder can handle touches (so does UIWebView). Read the doc:
http://developer.apple.com/library/ios/#documentation/uikit/reference/UIResponder_Class/Reference/Reference.html
You'll have to use:
touchesBegan:withEvent:
Edit: Adding the comment here for clarity-
I believe then there's no clean way of doing it, you can either override the hittest withEvent method like this or do a hack like this: overriding UIView
Do you mean you want to override the options that popup when they hold down on a link? I managed to get one to work with this tutorial/guide but the one posted here is still slightly buggy and needs you to do some fine tuning:
http://www.icab.de/blog/2010/07/11/customize-the-contextual-menu-of-uiwebview/

Scrolling with two fingers with a UIScrollView

I have an app where my main view accepts both touchesBegan and touchesMoved, and therefore takes in single finger touches, and drags. I want to implement a UIScrollView, and I have it working, but it overrides the drags, and therefore my contentView never receives them. I'd like to implement a UIScrollview, where a two finger drag indicates a scroll, and a one finger drag event gets passed to my content view, so it performs normally. Do I need create my own subclass of UIScrollView?
Here's my code from my appDelegate where I implement the UIScrollView.
#implementation MusicGridAppDelegate
#synthesize window;
#synthesize viewController;
#synthesize scrollView;
- (void)applicationDidFinishLaunching:(UIApplication *)application {
// Override point for customization after app launch
//[application setStatusBarHidden:YES animated:NO];
//[window addSubview:viewController.view];
scrollView.contentSize = CGSizeMake(720, 480);
scrollView.showsHorizontalScrollIndicator = YES;
scrollView.showsVerticalScrollIndicator = YES;
scrollView.delegate = self;
[scrollView addSubview:viewController.view];
[window makeKeyAndVisible];
}
- (void)dealloc {
[viewController release];
[scrollView release];
[window release];
[super dealloc];
}
In SDK 3.2 the touch handling for UIScrollView is handled using Gesture Recognizers.
If you want to do two-finger panning instead of the default one-finger panning, you can use the following code:
for (UIGestureRecognizer *gestureRecognizer in scrollView.gestureRecognizers) {
if ([gestureRecognizer isKindOfClass:[UIPanGestureRecognizer class]]) {
UIPanGestureRecognizer *panGR = (UIPanGestureRecognizer *) gestureRecognizer;
panGR.minimumNumberOfTouches = 2;
}
}
For iOS 5+, setting this property has the same effect as the answer by Mike Laurence:
self.scrollView.panGestureRecognizer.minimumNumberOfTouches = 2;
One finger dragging is ignored by panGestureRecognizer and so the one finger drag event gets passed to the content view.
In iOS 3.2+ you can now achieve two-finger scrolling quite easily. Just add a pan gesture recognizer to the scroll view and set its maximumNumberOfTouches to 1. It will claim all single-finger scrolls, but allow 2+ finger scrolls to pass up the chain to the scroll view's built-in pan gesture recognizer (and thus allow normal scrolling behavior).
UIPanGestureRecognizer *panGestureRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:#selector(recognizePan:)];
panGestureRecognizer.maximumNumberOfTouches = 1;
[scrollView addGestureRecognizer:panGestureRecognizer];
[panGestureRecognizer release];
You need to subclass UIScrollView (of course!). Then you need to:
make single-finger events to go to your content view (easy), and
make two-finger events scroll the scroll view (may be easy, may be hard, may be impossible).
Patrick's suggestion is generally fine: let your UIScrollView subclass know about your content view, then in touch event handlers check the number of fingers and forward the event accordingly. Just be sure that (1) the events you send to content view don't bubble back to UIScrollView through the responder chain (i.e. make sure to handle them all), (2) respect the usual flow of touch events (i.e. touchesBegan, than some number of {touchesBegan, touchesMoved, touchesEnded}, finished with touchesEnded or touchesCancelled), especially when dealing with UIScrollView. #2 can be tricky.
If you decide the event is for UIScrollView, another trick is to make UIScrollView believe your two-finger gesture is actually a one-finger gesture (because UIScrollView cannot be scrolled with two fingers). Try passing only the data for one finger to super (by filtering the (NSSet *)touches argument — note that it only contains the changed touches — and ignoring events for the wrong finger altogether).
If that does not work, you are in trouble. Theoretically you can try to create artificial touches to feed to UIScrollView by creating a class that looks similar to UITouch. Underlying C code does not check types, so maybe casting (YourTouch *) into (UITouch *) will work, and you will be able to trick UIScrollView into handling the touches that did not really happen.
You probably want to read my article on advanced UIScrollView tricks (and see some totally unrelated UIScrollView sample code there).
Of course, if you can't get it to work, there's always an option of either controlling UIScrollView's movement manually, or use an entirely custom-written scroll view. There's TTScrollView class in Three20 library; it does not feel good to the user, but does feel good to programmer.
This answers are a mess since you can only find the correct answer by reading all the other answers and the comments (closest answer got the question backwards). The accepted answer is too vague to be useful, and suggests a different method.
Synthesizing, this works
// makes it so that only two finger scrolls go
for (id gestureRecognizer in self.gestureRecognizers) {
if ([gestureRecognizer isKindOfClass:[UIPanGestureRecognizer class]])
{
UIPanGestureRecognizer *panGR = gestureRecognizer;
panGR.minimumNumberOfTouches = 2;
panGR.maximumNumberOfTouches = 2;
}
}
This requires two fingers for a scroll. I've done this in a subclass, but if not, just replace self.gestureRecognizers with myScrollView.gestureRecognizers and you're good to go.
The only thing that I added is using id to avoid an ugly cast :)
This works but can get quite messy if you want your UIScrollView to do zoom too... the gestures don't work correctly, since pinch-to-zoom and scroll fight it out. I'll update this if I find a suitable answer.
we managed to implement similar functionality in our iPhone drawing app by subclassing UIScrollView and filtering events depending on number of touches in simple and rude way:
//OCRScroller.h
#interface OCRUIScrollView: UIScrollView
{
double pass2scroller;
}
#end
//OCRScroller.mm
#implementation OCRUIScrollView
- (id)initWithFrame:(CGRect)aRect {
pass2scroller = 0;
UIScrollView* newv = [super initWithFrame:aRect];
return newv;
}
- (void)setupPassOnEvent:(UIEvent *)event {
int touch_cnt = [[event allTouches] count];
if(touch_cnt<=1){
pass2scroller = 0;
}else{
double timems = double(CACurrentMediaTime()*1000);
pass2scroller = timems+200;
}
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
[self setupPassOnEvent:event];
[super touchesBegan:touches withEvent:event];
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
[self setupPassOnEvent:event];
[super touchesMoved:touches withEvent:event];
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
pass2scroller = 0;
[super touchesEnded:touches withEvent:event];
}
- (BOOL)touchesShouldBegin:(NSSet *)touches withEvent:(UIEvent *)event inContentView:(UIView *)view
{
return YES;
}
- (BOOL)touchesShouldCancelInContentView:(UIView *)view
{
double timems = double(CACurrentMediaTime()*1000);
if (pass2scroller == 0 || timems> pass2scroller){
return NO;
}
return YES;
}
#end
ScrollView setuped as follows:
scroll_view = [[OCRUIScrollView alloc] initWithFrame:rect];
scroll_view.contentSize = img_size;
scroll_view.contentOffset = CGPointMake(0,0);
scroll_view.canCancelContentTouches = YES;
scroll_view.delaysContentTouches = NO;
scroll_view.scrollEnabled = YES;
scroll_view.bounces = NO;
scroll_view.bouncesZoom = YES;
scroll_view.maximumZoomScale = 10.0f;
scroll_view.minimumZoomScale = 0.1f;
scroll_view.delegate = self;
self.view = scroll_view;
simple tap does nothing (you can handle it in the way you need), tap with two fingers scrolls/zooms view as expected. no GestureRecognizer is used, so works from iOS 3.1
I've got a further improvement to the code above. The problem was, that even after we set setCanCancelContentTouches:NO We have the problem, that a zoom gesture will interrupt with the content. It won't cancel the content touch but allow zooming in the meantime. TO prevent this i lock the zooming by setting the minimumZoomScale and maximumZoomScale to the same values everytime, the timer fires.
A quite strange behavior is that when a one finger event gets canceled by a two finger gesture within the allowed time period, the timer will be delayed. It gets fired after the touchCanceled Event gets called. So we have the problem, that we try to lock the zooming although the event is already canceled and therefore disable zooming for the next event.
To handle this behavior the timer callback method checks against if touchesCanceled was called before.
#implementation JWTwoFingerScrollView
#pragma mark -
#pragma mark Event Passing
- (id)initWithCoder:(NSCoder *)coder {
self = [super initWithCoder:coder];
if (self) {
for (UIGestureRecognizer* r in self.gestureRecognizers) {
if ([r isKindOfClass:[UIPanGestureRecognizer class]]) {
[((UIPanGestureRecognizer*)r) setMaximumNumberOfTouches:2];
[((UIPanGestureRecognizer*)r) setMinimumNumberOfTouches:2];
zoomScale[0] = -1.0;
zoomScale[1] = -1.0;
}
timerWasDelayed = NO;
}
}
return self;
}
-(void)lockZoomScale {
zoomScale[0] = self.minimumZoomScale;
zoomScale[1] = self.maximumZoomScale;
[self setMinimumZoomScale:self.zoomScale];
[self setMaximumZoomScale:self.zoomScale];
NSLog(#"locked %.2f %.2f",self.minimumZoomScale,self.maximumZoomScale);
}
-(void)unlockZoomScale {
if (zoomScale[0] != -1 && zoomScale[1] != -1) {
[self setMinimumZoomScale:zoomScale[0]];
[self setMaximumZoomScale:zoomScale[1]];
zoomScale[0] = -1.0;
zoomScale[1] = -1.0;
NSLog(#"unlocked %.2f %.2f",self.minimumZoomScale,self.maximumZoomScale);
}
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
NSLog(#"began %i",[event allTouches].count);
[self setCanCancelContentTouches:YES];
if ([event allTouches].count == 1){
touchesBeganTimer = [NSTimer scheduledTimerWithTimeInterval:0.1 target:self selector:#selector(firstTouchTimerFired:) userInfo:nil repeats:NO];
[touchesBeganTimer retain];
[touchFilter touchesBegan:touches withEvent:event];
}
}
//if one finger touch gets canceled by two finger touch, this timer gets delayed
// so we can! use this method to disable zooming, because it doesnt get called when two finger touch events are wanted; otherwise we would disable zooming while zooming
-(void)firstTouchTimerFired:(NSTimer*)timer {
NSLog(#"fired");
[self setCanCancelContentTouches:NO];
//if already locked: unlock
//this happens because two finger gesture delays timer until touch event finishes.. then we dont want to lock!
if (timerWasDelayed) {
[self unlockZoomScale];
}
else {
[self lockZoomScale];
}
timerWasDelayed = NO;
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
// NSLog(#"moved %i",[event allTouches].count);
[touchFilter touchesMoved:touches withEvent:event];
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
NSLog(#"ended %i",[event allTouches].count);
[touchFilter touchesEnded:touches withEvent:event];
[self unlockZoomScale];
}
//[self setCanCancelContentTouches:NO];
-(void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
NSLog(#"canceled %i",[event allTouches].count);
[touchFilter touchesCancelled:touches withEvent:event];
[self unlockZoomScale];
timerWasDelayed = YES;
}
#end
Bad news: iPhone SDK 3.0 and up, don't pass touches to -touchesBegan: and -touchesEnded: **UIScrollview**subclass methods anymore. You can use the touchesShouldBegin and touchesShouldCancelInContentView methods that is not the same.
If you really want to get this touches, have one hack that allow this.
In your subclass of UIScrollView override the hitTest method like this:
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
UIView *result = nil;
for (UIView *child in self.subviews)
if ([child pointInside:point withEvent:event])
if ((result = [child hitTest:point withEvent:event]) != nil)
break;
return result;
}
This will pass to you subclass this touches, however you can't cancel the touches to UIScrollView super class.
What I do is have my view controller set up the scroll view:
[scrollView setCanCancelContentTouches:NO];
[scrollView setDelaysContentTouches:NO];
And in my child view I have a timer because two-finger touches usually start out as one finger followed quickly by two fingers.:
- (void) touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
// Hand tool or two or more touches means a pan or zoom gesture.
if ((selectedTool == kHandToolIndex) || (event.allTouches.count > 1)) {
[[self parentScrollView] setCanCancelContentTouches:YES];
[firstTouchTimer invalidate];
firstTouchTimer = nil;
return;
}
// Use a timer to delay first touch because two-finger touches usually start with one touch followed by a second touch.
[[self parentScrollView] setCanCancelContentTouches:NO];
anchorPoint = [[touches anyObject] locationInView:self];
firstTouchTimer = [NSTimer scheduledTimerWithTimeInterval:kFirstTouchTimeInterval target:self selector:#selector(firstTouchTimerFired:) userInfo:nil repeats:NO];
firstTouchTimeStamp = event.timestamp;
}
If a second touchesBegan: event comes in with more than one finger, the scroll view is allowed to cancel touches. So if the user pans using two fingers, this view would get a touchesCanceled: message.
This seems to be the best resource for this question on the internet. Another close solution can be found here.
I have solved this issue in a very satisfactory manner in a different way, essentially by supplanting my own gesture recognizer into the equation. I strongly recommend that anyone who is trying to achieve the effect requested by the original poster consider this alternative over aggressive subclassing of UIScrollView.
The following process will provide:
A UIScrollView containing your custom view
Zoom and Pan with two fingers (via UIPinchGestureRecognizer)
Your view's event processing for all other touches
First, let's assume you have a view controller and its view. In IB, make the view a subview of a scrollView and adjust the resize rules of your view so that it does not resize. In the attributes of the scrollview, turn on anything that says "bounce" and turn off "delaysContentTouches". Also you must set the zoom min and max to other than the default of 1.0 for, as Apple's docs say, this is required for zooming to work.
Create a custom subclass of UIScrollView, and make this scrollview that custom subclass. Add an outlet to your view controller for the scrollview and connect them up. You're now totally configured.
You will need to add the following code to the UIScrollView subclass so that it transparently passes touch events (I suspect this could be done more elegantly, perhaps even bypassing the subclass altogether):
#pragma mark -
#pragma mark Event Passing
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
[self.nextResponder touchesBegan:touches withEvent:event];
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
[self.nextResponder touchesMoved:touches withEvent:event];
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
[self.nextResponder touchesEnded:touches withEvent:event];
}
- (BOOL)touchesShouldCancelInContentView:(UIView *)view {
return NO;
}
Add this code to your view controller:
- (void)setupGestures {
UIPinchGestureRecognizer *pinchGesture = [[UIPinchGestureRecognizer alloc] initWithTarget:self action:#selector(handlePinchGesture:)];
[self.view addGestureRecognizer:pinchGesture];
[pinchGesture release];
}
- (IBAction)handlePinchGesture:(UIPinchGestureRecognizer *)sender {
if ( sender.state == UIGestureRecognizerStateBegan ) {
//Hold values
previousLocation = [sender locationInView:self.view];
previousOffset = self.scrollView.contentOffset;
previousScale = self.scrollView.zoomScale;
} else if ( sender.state == UIGestureRecognizerStateChanged ) {
//Zoom
[self.scrollView setZoomScale:previousScale*sender.scale animated:NO];
//Move
location = [sender locationInView:self.view];
CGPoint offset = CGPointMake(previousOffset.x+(previousLocation.x-location.x), previousOffset.y+(previousLocation.y-location.y));
[self.scrollView setContentOffset:offset animated:NO];
} else {
if ( previousScale*sender.scale < 1.15 && previousScale*sender.scale > .85 )
[self.scrollView setZoomScale:1.0 animated:YES];
}
}
Please note that in this method there are references to a number of properties you must define in your view controller's class files:
CGFloat previousScale;
CGPoint previousOffset;
CGPoint previousLocation;
CGPoint location;
Ok that's it!
Unfortunately I could not get the scrollView to show its scrollers during the gesture. I tried all of these strategies:
//Scroll indicators
self.scrollView.showsVerticalScrollIndicator = YES;
self.scrollView.showsVerticalScrollIndicator = YES;
[self.scrollView flashScrollIndicators];
[self.scrollView setNeedsDisplay];
One thing I really enjoyed is if you'll look at the last line you'll note that it grabs any final zooming that's around 100% and just rounds it to that. You can adjust your tolerance level; I had seen this in Pages' zoom behavior and thought it would be a nice touch.
I put this in the viewDidLoad method and this accomplishes the scroll view handling the two touch pan behavior and another pan gesture handler handling the one touch pan behavior -->
scrollView.panGestureRecognizer.minimumNumberOfTouches = 2
let panGR = UIPanGestureRecognizer(target: self, action: #selector(ViewController.handlePan(_:)))
panGR.minimumNumberOfTouches = 1
panGR.maximumNumberOfTouches = 1
scrollView.gestureRecognizers?.append(panGR)
and in the handlePan method which is a function attached to the ViewController there is simply a print statement to verify that the method is being entered -->
#IBAction func handlePan(_ sender: UIPanGestureRecognizer) {
print("Entered handlePan numberOfTuoches: \(sender.numberOfTouches)")
}
HTH
Check out my solution:
#import “JWTwoFingerScrollView.h”
#implementation JWTwoFingerScrollView
- (id)initWithCoder:(NSCoder *)coder {
self = [super initWithCoder:coder];
if (self) {
for (UIGestureRecognizer* r in self.gestureRecognizers) {
NSLog(#“%#”,[r class]);
if ([r isKindOfClass:[UIPanGestureRecognizer class]]) {
[((UIPanGestureRecognizer*)r) setMaximumNumberOfTouches:2];
[((UIPanGestureRecognizer*)r) setMinimumNumberOfTouches:2];
}
}
}
return self;
}
-(void)firstTouchTimerFired:(NSTimer*)timer {
[self setCanCancelContentTouches:NO];
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
[self setCanCancelContentTouches:YES];
if ([event allTouches].count == 1){
touchesBeganTimer = [NSTimer scheduledTimerWithTimeInterval:0.1 target:self selector:#selector(firstTouchTimerFired:) userInfo: nil repeats:NO];
[touchesBeganTimer retain];
[touchFilter touchesBegan:touches withEvent:event];
}
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
[touchFilter touchesMoved:touches withEvent:event];
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
NSLog(#“ended %i”,[event allTouches].count);
[touchFilter touchesEnded:touches withEvent:event];
}
-(void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
NSLog(#“canceled %i”,[event allTouches].count);
[touchFilter touchesCancelled:touches withEvent:event];
}
#end
It does not delays the first touch and does not stop when the user touches with two fingers after using one. Still it allows to cancel a just started one touch event using a timer.
Yes, you'll need to subclass UIScrollView and override its -touchesBegan: and -touchesEnded: methods to pass touches "up". This will probably also involve the subclass having a UIView member variable so that it knows what it's meant to pass the touches up to.
Kenshi's answer in Swift 4
for gestureRecognizer: UIGestureRecognizer in self.gestureRecognizers! {
if (gestureRecognizer is UIPanGestureRecognizer) {
let panGR = gestureRecognizer as? UIPanGestureRecognizer
panGR?.minimumNumberOfTouches = 2
}
}