views:

170

answers:

2

Hi,

I'd like to handle touches (horizontal slide in particular) in a UIViewController subclass I did, but the problem is the controller's view contains a UIScrollView which covers the whole screen, so "touchesBegan:" and "touchesEnded:" events are never called in my controller. The scrollview only scrolls vertically.

What would be the best way to make it able to handle these events? Make my UIViewController also inherit from UIScrollView and handle touches as needed (and remove the current scroll view), or use a UIScrollView subclass in place of my current scroll view, which will call a delegate method for the cases I need? Or maybe is there a another way?

Here is my current code:

// View creation and adding to parent
NewViewController* newViewController = [[newViewController alloc] initWithNibName:@"NewViewController" bundle:nil];
[self.view addSubview:newViewController.view];

// NewViewController.h
@interface NewViewController : UIViewController {
    IBOutlet UIScrollView*  scrollView;

    // other outlets and properties
}
- (void)touchesBegan:(NSSet*)touches withEvent:(UIEvent*)event;
- (void)touchesEnded:(NSSet*)touches withEvent:(UIEvent*)event;

In IB, NewViewController's view contains scrollView which contains all other outlets (labels, buttons)... The goal would be to get NewViewController's scrollView to call NewViewController's touchesBegan and touchesEnded methods.

Update: The ScrollViewSuite helped me a lot to understand how the touches callbacks are handled. UIScrollView only calls them when the touches don't make it scroll.

So, three cases:

  • Oblique slide (even slightly, as soon as scrollbar appears): no touches method is called
  • Perfectly horizontal slide (no scrollbar appears): touchesBegan, touchesMoved and touchesEnded are called
  • Any touch that doesn't cause a scroll (touch without moving, or horizontal move) followed by a slightly vertical slide: touchesBegan, touchesMoved and touchesCancelled are called;

Touches cancel can be avoided by setting the UIScrollView's canCancelContentTouches to FALSE but this obviously only works if touchesBegan has been called, which means touches methods will still not be called for the first case.

But UIScrollView has a property called delaysContentTouches which is set at TRUE by default, and causes it to wait a little bit when a touch begins in order to see if it will make him scroll. If this is set to FALSE, touches methods are called as soon as the touch starts and allows a subclasses to handle them as they wish.

TL;DR: What seems to be the easiest way to me (if you can't use UIGestureRecognizer) is to subclass UIScrollView, set its delayContentTouches property to FALSE and override touches functions (touchesShouldBegin / touchesShouldCancel as well as touchesBegan / touchesMoved...) according to your needs.

+1  A: 

Your viewController's view should be a subview of UIScrollView. Create a UIScrollView add as a subView of the window, then create an instance of your viewController that has the slider and add to the scrollView's subview.

UIScrollView *scrollView = [[UIScrollView alloc] initWithFrame:[window bounds]];
[window addSubView:scrollView];
UIViewController *vc = [[UIViewController alloc] initWithNibNamed:@"MyVC" bundle:nil];
[scrollView addSubView:[vc view]];

I have a suspicion that you are not receiving the events as a result of missing connections in the IB or that the scrollView is forwarding events to the superview of newViewController's view.

In the Apple docs for UIResponder's nextResponder property.

The UIResponder class does not store or set the next responder automatically, instead returning nil by default. Subclasses must override this method to set the next responder. UIView implements this method by returning the UIViewController object that manages it (if it has one) or its superview (if it doesn’t); UIViewController implements the method by returning its view’s superview; UIWindow returns the application object, and UIApplication returns nil.

Couple of things to try.

  1. In NewViewController.m, override canBecomeFirstResponder
    
    - (BOOL)canBecomeFirstResponder
    {
        return YES;
    }
    
  2. Because you are just adding your new view as a subview instead of using navigation or modal views make sure the superview of newViewController is no longer the first responder. Add:
    
    // View creation and adding to parent
    NewViewController* newViewController = [[newViewController alloc] initWithNibName:@"NewViewController" bundle:nil];
    [self.view addSubview:newViewController.view];
    [self.view resignFirstResponder];
    
  3. Make sure that the `NextViewController's` view is connected to File's Owner in IB.
  4. Make sure that the userInteractionEnabled property is YES for the view

Take a look at the ScrollViewSuite sample code as well. I noticed that controller containing the view that receives events from the scrollView implements delegate methods for the view. An approach that you can try is to subclass the views inside the scrollView and delegate the event handling to your controller as per the sample code.

There are also samples for the new UIGesturizers discussed in other answers. Note that guesturizers are added to views and the handling of the guesturizers are delegated to another object (ususally the view's controller)

You stated that the buttons and labels are added as subviews of the scrollview in IB. It seems that you should have a UIView as the container for you buttons and labels. Just a thought as I am not able to tell from the code snippet.

UIScrollView
  -> UIView
      ->UIButton
      ->UILabel
      ..
      ..
falconcreek
Hum, I'm not sure I get your point.Thing is my viewControllers' view is already a subview of another VC view from a previous screen in the app, so I tried to remove the scrollView from the nib, create it manually like you do in my current VC's viewDidLoad add it to the VC's superview, and finally add the VC's view to scrollView's subviews. But this does nothing, I get neither scrolling nor touchesBegan/Ended callbacks.
Jukurrpa
post your code then
falconcreek
I updated my question with the original code. There isn't much to see though.
Jukurrpa
I overrode canBecomeFirstResponder, resigned the superview's first responder and checked everything in IB, touches methods are still not getting called. I've tried what I was thinking about first, I created a subclass of UIScrollView which overrides touches methods (stores some data before calling [super touches...]) and replaced my scrollView with it. This works, but these methods aren't called every time... It is even pretty random. I have to slide slowly and avoid scrolling if I want to be sure both methods are called, as if there was some kind of filter... Does not seem like a good method.
Jukurrpa
Added a few more things to try
falconcreek
Sorry for the delay, I was working on other things.I think I've found the solution, I updated the original question. Now I'll see if handling everything manually is feasible.Thanks a lot for your help, ScrollViewSuite was a good idea.
Jukurrpa
+4  A: 

If you are building for iPhone OS 3.2 or above, you can use the Gesture Recognizer objects. From your description, it sounds like you would want either a UIPanGestureRecognizer or a UISwipeGestureRecognizer. Instantiate the appropriate object, add it to your viewController's view with the [addGestureRecognizer:] method and implement the delegate methods in your viewController.

For reference:
iPhone Event Handling Guide: Gesture Recognizers
UIGestureRecognizer Reference (abstract superclass)
UIGestureRecognizerDelegate Protocol Reference

Endemic
Well I used to build for 3.1.3 iOS and the iPhone I work on is currently at 3.1.3 too. In case I can't find anything simple I'll try to upgrade and look into these Gesture Recognizers, thanks.
Jukurrpa