Scrolling with two fingers with a UIScrollView - iphone
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
}
}
Related
Subview outside of main view does not receive touch events
In my application i have a main screen, which has a subview (this is another screen), but this subview is moved 320 to the right so it is just offscreen. The effect is that the main screen moves 320 to the left, which hides the main screen and shows the other screen because it's a subview. The problem is, the other screen is not receiving touch events because it is out of the bounds of the main screen (superview). How do i get this other screen to also receive touch events so i can tap the buttons?
Have a look at this post on S.O. for an explanation. The solution is to implement your own hitTest using the following code: - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event { BOOL isInside = [super pointInside:point withEvent:event]; // identify the button view subclass UIButton *b = (UIButton *)[self viewWithTag:3232]; CGPoint inButtonSpace = [self convertPoint:point toView:b]; BOOL isInsideButton = [b pointInside:inButtonSpace withEvent:nil]; if (YES == isInsideButton) { return isInsideButton; } // if (YES == isInsideButton) return isInside; } - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event { UIView *v = nil; v = [super hitTest:point withEvent:event]; return v; }
Detect touch on uiview under the UIButton when button is pressed
i am using a uiview parent view....overidded touch began and touch ended methods... when i added a UIButton as a subview in the view and touch on the button touchdown event is detected and associated method is called ...but touchbegan method that was over ridden is not called... what i want is when i touch the button the method associated with the touchdown event and touchBegan method of uiview both be called simultaneously...UIbutton is not passing the touch to its superview i.e. uiview.....? Any idea how to do that ? Thanks
I'm not sure exactly how to call two touchesBegan events simultaneously on two different views, but you probably want to override the hitTest:withEvent: method of UIView. This will allow you to catch a touch event on the view underneath the UIButton before it gets to the button (hitTests work from the window upwards to the foremost view). - (UIView*)hitTest:(CGPoint)point withEvent:(UIEvent *)event { if (passToSubviews) { return [super hitTest: point withEvent: event]; } // otherwise stop the subview receiving touches if ([super hitTest: point withEvent: event]) { return self; } return nil; } Maybe this can help... EDIT: Just a guess but you could try: - (UIView*)hitTest:(CGPoint)point withEvent:(UIEvent *)event { UIView* hitSubview = [super hitTest: point withEvent: event]; if (hitSubview) { [self doTouchedInStuff]; return hitSubview; } return self; } - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { [self doTouchedInStuff]; [super touchesBegan:touches withEvent:event]; }
UIScrollView with UIImageView as subviews, taping UIImageView
I need to do something like this: UISCrollView with UIImageViews as its subviews. When user taps on UIImageViews an action occurs. But when user want to scroll UIScrollView should scroll even if scrolling started at UIImageView (at location where UIImage of UIImageView is displayed). Basicly I can get one of two scenarios: I can get that if user taps on UIImageView (which is subview of UIScrollView) an action occur, but when you try to scroll by draging finger from UIImageView the action also occurs (and I want a scroll to occur). I can make that regardles where user taps the view will scroll but if user taps UIImageView the action will NOT occur. I can't get you any of my code because I'm testing a lot of aprroches here and there and it's bit messy so it would be no use at all (without a tons of commenting). Is there a clean and simple solution for doing this? Ok here is some code: -(UIView*) hitTest:(CGPoint)point withEvent:(UIEvent *)event { if(isDragging == NO) { return [super hitTest:point withEvent:event]; } NSLog(#"dragging ><><><><><>>><><><><>"); return nil; } Now if I return nil then I can scroll but I can't get to tap my UIImageView for an action. If I return [super hitTest:point withEvent:event] I can't scroll over my UIIMageView. isDragging is test code to determine if I'm trying to scroll or just tap. But hit test occurs before I can set isDragging property accordingly to event that is happening. Here is my init -(id) initWithCoder:(NSCoder *)aDecoder { if(self = [super initWithCoder:aDecoder]) { [self setUserInteractionEnabled:YES]; UISwipeGestureRecognizer *swipeRecLeft = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:#selector(swipe)]; swipeRecLeft.direction = UISwipeGestureRecognizerDirectionDown; UISwipeGestureRecognizer *swipeRecRight = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:#selector(swipe)]; swipeRecRight.direction = UISwipeGestureRecognizerDirectionUp; UITapGestureRecognizer *singleTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(tapSingle)]; [self addGestureRecognizer:swipeRecRight]; [self addGestureRecognizer:swipeRecLeft]; [self addGestureRecognizer:singleTap]; [swipeRecLeft release]; [swipeRecRight release]; [singleTap release]; isDragging = NO; } else { self = nil; } return self; } Here are the rest of actions -(void) tapSingle { [self.delegate hitOccur]; } -(void) swipe { isDragging = YES; } And on top of it in my UIScrollView I've setted delegate and when scrolling ends I set isDragging property manualy to NO on each subview of my UIScrollView. It's working... but it's not perfect. To actually scroll content I must swipe TWICE in UIImageView (first one is to set isDragging to YES and then we can scroll...). How to this right and proper? LATEST UPDATE: Ok I've managed to solve this problem. However I'm dead sure my way isn't clean or good one (but regardles it works). In my UIScrollView subclass I've overided hitTest method with this: -(UIView*) hitTest:(CGPoint)point withEvent:(UIEvent *)event { if(!self.dragging) { if([[super hitTest:point withEvent:event] class] == [ResponsiveBookView class]) { container = [super hitTest:point withEvent:event]; } } return self; } Where the container is id container and it holds my UIView subclass. So I can recognize if the touch was on my image or on scroll view itself. Now I need to detec if it is scrolling or just touching and I do this here: -(void) touchesEnded: (NSSet *) touches withEvent: (UIEvent *) event { if (!self.dragging) { NSLog(#"touch touch touch"); [container tapSingle]; [self.nextResponder touchesEnded: touches withEvent:event]; } [super touchesEnded: touches withEvent: event]; } As you see if self.dragging (scrolling) the default behaviour will apply. If !self.dragging I will manually call tapSingle on my container (which will then make an action "occur"). It works!
Yeah—use gesture recognizers. If you add a UITapGestureRecognizer to each UIImageView (using the UIVIew -addGestureRecognizer: method), you should get both recognition of the taps and the default scrolling behavior.
Hiding the keyboard when UITextField loses focus
I've seen some threads about how to dismiss the Keyboard when a UITextField loses focus, but it did not work for me and I don't know how. The "touchesBegan:withEvent:" in the following code, never gets called. Why? - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { UITouch *touch = [[event allTouches] anyObject]; if ([self.textFieldOnFocus isFirstResponder] && [touch view] != self.textFieldOnFocus) { [textFieldOnFocus resignFirstResponder]; } [super touchesBegan:touches withEvent:event]; } P.S.: This code has been inserted in the view controller which has a UITableView. The UITextField is in a cell from this table. So, my opinion is: this method is not being called, cause the touch occurs on the UITableView from my ViewController. So, I think, that to I should have to subclass the UITableView, to use this method as I have seen on other Threads, but it may have a easier way. Could you please help me? Thanks a lot!
Make sure you set the delegate of the UITextField to First Responder in IB. And I just put a custom (invisible) UIButton over the screen and set up an IBAction to hide the keyboard. Ex: - (IBAction)hideKeyboard { [someTextField resignFirstResponder]; } With that hooked up to a UIButton.
Here is my solution, somewhat inspired by several posts in SO: Simply handle the tap gesture in the context of the View, the user is 'obviously' trying to leave the focus of the UITextField. -(void)handleViewTapGesture:(UITapGestureRecognizer *)gesture { [self endEditing:YES]; } This is implemented in the ViewController. The handler is added as a gesture recognizer to the appropriate View in the View property's setter: -(void) setLoginView:(LoginView *)loginView { _loginView = loginView; UITapGestureRecognizer *tapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self.loginView action:#selector(handleTapGesture:)]; [tapRecognizer setDelegate:self]; // self implements the UIGestureRecognizerDelegate protocol [self.loginView addGestureRecognizer:tapRecognizer]; } The handler could be defined in the View as well. If you are unfamiliar with handling gestures, see Apple's docs are tons of samples elsewhere. I should mention that you will need some additional code to make sure other controls get taps, you need a delegate that implements the UIGestureRecognizerDelegate protocol and this method: - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch { if ([touch.view isKindOfClass:[UIButton class]]) // Customize appropriately. return NO; // Don't let the custom gestureRecognizer handle the touch return YES; }
-(void)touchesEnded: (NSSet *)touches withEvent: (UIEvent *)event { for (UIView* view in self.view.subviews) { if ([view isKindOfClass:[UITextField class]]) [view resignFirstResponder]; } }
Handling touches inside UIWebview
I have created a subclass of UIWebView , and have implemented the touchesBegan, touchesMoved and touchesEnded methods. but the webview subclass is not handling the touch events. Is there any method to handle the touch events inside the UIWebView subclass ???
No subclassing needed, just add a UITapGestureRecognizer : UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(didTapMethod)]; [tap setNumberOfTapsRequired:1]; // Set your own number here [tap setDelegate:self]; // Add the <UIGestureRecognizerDelegate> protocol [self.myWebView addGestureRecognizer:tap]; Add the <UIGestureRecognizerDelegate> protocol in the header file, and add this method: - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer { return YES; }
If all you need is to handle gestures, while leaving the rest of the UIWebView functionality intact, you can subclass UIWebView and use this strategy: in the init method of your UIWebView subclass, add a gesture recognizer, e.g.: UISwipeGestureRecognizer * swipeRight = [[UISwipeGestureRecognizer alloc]initWithTarget:self action:#selector(handleSwipeGestureRightMethod)]; swipeRight.direction = UISwipeGestureRecognizerDirectionRight; [self addGestureRecognizer:swipeRight]; swipeRight.delegate = self; then, add this method to your class: - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer{ return YES; } Add and handle your designated selector to the class, in this case "handleSwipeGestureRightMethod" and you are good to go...
You could put an UIView over your UIWebView, and overide the touchesDidBegin etc, then send them to your webview. Ex: User touches your UIView, which provokes a - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { // Execute your code then send a touchesBegan to your webview like so: [webView touchesBegan:touches withEvent:event]; return; } your UIView has to be over the webview.
I'm not sure if this is what you want (it's not what you asked for, but it might work depending on what your end game is), but you could instead interpret the touches in JavaScript from inside the UIWebView, and get javascript to do document.location='http://null/'+xCoord+'/'+yCoord; // Null is arbitrary. Then you can catch that using the UIWebView's delegate method - (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType And if the request.URL.host (or whatever it is) isEqualToString:#"null" take the relevant action (and return NO instead of YES). You can even add the JS to each page by doing something like: - (void)webViewDidFinishLoad:(UIWebView *)webView { [webView stringByEvaluatingJavaScriptFromString:#"window.ontouchstart=function(/* ... */);"]; } Hope this helps?
Handling gestures on a UIWebView is discussed in this Apple Developer forum thread. Using the info given there, there will be no need for an extra view in most or all cases, and as mentioned here before, overriding UIWebView is not the way to go. Copypaste of the most important post in the thread: This is a known issue. The UIWebView has its own UITapGestureRecognizers, and they're on a private subview of the UIWebView itself. UIGestureRecognizer precedence defines that gestures attached to views deeper in the view hierarchy will exclude ones on superviews, so the web view's tap gestures will always win over yours. If it's okay in your case to allow your tap to happen along with the web view's normal tap your best solution would be to implement the UIGestureRecognizerDelegate method gestureRecognizer:shouldRecognizeSimultaneouslyWithGestureRecognizer and return YES for other tap gestures. This way you'll get your tap handler called, and the web view will still get its called. If you need to be the only one handling the tap you'll have to subclass UITapGestureRecognizer so you can use the one-way overrides in UIGestureRecognizerSubclass.h, an you can then return NO from canBePreventedByGestureRecognizer: when asked if the web view's tap gesture recognizer can prevent yours. In any case, we know about this and hope to make it easier in the future.
I've just found that UIWebView does check whether it responds to the - (void)webViewDidNotClick: (id)webBrowserView selector, once one taps on the view area (not on hyperref, or any other area that should be handled specifically). So you may implement that selector with your handling code :)
Do you mean your sub-classed implementation is not called when touchesBegan, touchesMoved and touchesEnded are called? It sounds like a problem with how you've created an instance of the object. More details are required I think. (taken form comments) Header File #import <UIKit/UIKit.h> #interface MyWebView : UIWebView { } #end Implementation File #import "MyWebView.h" #implementation MyWebView - (id)initWithFrame:(CGRect)frame { if (self = [super initWithFrame:frame]) { } return self; } - (void)drawRect:(CGRect)rect { NSLog(#"MyWebView is loaded"); } - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { NSLog(#"touches began"); } - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { NSLog(#"Touches ended"); } - (void)dealloc { [super dealloc]; } #end
I would try overriding -sendEvent: on UIWindow, to see if you can intercept those touch events.
Following on from what Unfalkster said, you can use the hitTest method to achieve what you want, but you don't have to subclass UIWindow. Just put this in your web view subclass. You will get a compile time warning but it does work: - (void)hitTest:(CGPoint)point withEvent:(UIEvent *)event { if (event.type == UIEventTypeTouches) { // get location info CGFloat x = point.x; CGFloat y = point.y; // get touches NSSet *touches = [event allTouches]; // individual touches for (UITouch *touch in touches) { if (touch.phase == UITouchPhaseBegan) { // touches began } else if (touch.phase == UITouchPhaseMoved) { } // etc etc } } // call the super [super hitTest:point withEvent:event]; } Hope that helps!
If you want to detect your own taps but disable the UIWebView's taps then you can use my solution: -(void)recursivelyDisableTapsOnView:(UIView*)v{ for(UIView* view in v.subviews){ for(UIGestureRecognizer* g in view.gestureRecognizers){ if(g == self.ownTapRecognizer){ continue; } if([g isKindOfClass:[UITapGestureRecognizer class]] || [g isKindOfClass:[UILongPressGestureRecognizer class]] || [g isKindOfClass:NSClassFromString(#"UITapAndAHalfRecognizer")]){ g.enabled = NO; } } [self recursivelyDisableTapsOnView:view]; } } - (void)webViewDidFinishLoad:(UIWebView *)webView{ [self recursivelyDisableTapsOnView:webView]; //disable selection [webView stringByEvaluatingJavaScriptFromString:#"document.documentElement.style.webkitUserSelect='none';"]; // Disable callout [webView stringByEvaluatingJavaScriptFromString:#"document.documentElement.style.webkitTouchCallout='none';"]; }