Scrolling with two fingers with a UIScrollView

Viewed 26172

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];
}
14 Answers

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.

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.

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

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.

Related