views:

3657

answers:

7

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];
}
+1  A: 

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.

Patrick McCafferty
Can you clarify what you mean Patrick? I have subclassed UIScrollView, but I don't know how to implement the scrolling myself (move the frame of the scrollview?) and I'm not sure how to pass events to my UIView (content). Right now, it's Responder methods are never being called.
Craig
What you would have is a UIView member variable in the UIScrollView subclass that you assign to when creating the subclass.Then, implement -touchesBegan, -touchesMoved and -touchesEnded and have them simply pass "up" the event (by calling [uiViewMemberVariable touchesBegan:...]) unless they know it's for them.
Patrick McCafferty
Oh, and if you know it's for the ScrollView then just do [super touchesBegan:...] in that.
Patrick McCafferty
+1  A: 

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.

Andrey Tarantsov
Andrey, Thanks for your explanation. I actually got it working by passing a subset of the touches to super... I created an NSSet of just one touch event, and passed that. However, should I be passing the same UIEvent in that call? Or do I need modify something in the event before passing it? I'm getting some unexpected behavior with scrolling back, and I think it's because of the UIevent I'm passing to super is the original event and the NSSet of touches is modified to have only one touch.
Craig
Craid, I don't believe you can modify UIEvent, so if you are right about the cause, you are in trouble. :)
Andrey Tarantsov
+1  A: 

Bad news: iPhone SDK 3.0 and up, don't pass touches to -touchesBegan: and -touchesEnded: UIScrollviewsubclass 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.

SEQOY Development Team
+1  A: 

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.

lucius
+1  A: 

This seems to be the best resource for this question on the internet. Another close solution includes http://www.iphonedevsdk.com/forum/iphone-sdk-development/6686-uiscrollview-wont-respond-touch-event-scrolls-pans-but-tochesshouldbegin-ne.html

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 "delays content touches". 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.

SG1
+3  A: 

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;               
    }
    }
Kenshi
I tried this, but the scrollview is, for some reason, detecting any multi-touches as separate single touches (i.e: a two finger swipe is detected as two single fingers), so the gesture is never called. Any ideas?
Dyldo42
+2  A: 

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];
Mike Laurence
Oh wow, AWESOME!
Colin Barrett