views:

10616

answers:

7

I'm trying to create a springboard-like interface within my app. I'm trying to use UIButtons added to a UIScrollView. The problem I'm running in to is with the buttons not passing any touches to the UIScrollView - if I try to flick/slide and happen to press on the button it doesn't register for the UIScrollView, but if I flick the space between buttons it will work. The buttons do click/work if I touch them.

Is there a property or setting that forces the button to send the touch events up to its parent (superview)? Do the buttons need to be added to something else before being added the UIScrollView?

Here is my code:

 //init scrolling area
 UIScrollView *scrollView = [[UIScrollView alloc] initWithFrame:CGRectMake(0, 0, 480, 480)];
 scrollView.contentSize = CGSizeMake(480, 1000);
 scrollView.bounces = NO;
 scrollView.delaysContentTouches = NO;

 //create background image
 UIImageView *rowsBackground = [[UIImageView alloc] initWithImage:[self scaleAndRotateImage:[UIImage imageNamed:@"mylongbackground.png"]]];
 rowsBackground.userInteractionEnabled = YES;

 //create button
      UIButton *btn = [[UIButton buttonWithType:UIButtonTypeCustom] retain];
      btn.frame = CGRectMake(100, 850, 150, 150);
 btn.bounds = CGRectMake(0, 0, 150.0, 150.0);
 [btn setImage:[self scaleAndRotateImage:[UIImage imageNamed:@"basicbutton.png"]] forState:UIControlStateNormal];
      [btn addTarget:self action:@selector(buttonClick) forControlEvents:UIControlEventTouchUpInside];

 //add "stuff" to scrolling area
 [scrollView addSubview:rowsBackground];
 [scrollView addSubview:btn];

 //add scrolling area to cocos2d
 //this is just a UIWindow
 [[[Director sharedDirector] openGLView] addSubview:scrollView];

 //mem-mgmt
 [rowsBackground release];
 [btn release];
 [scrollView release];
+1  A: 

UIScrollView handles a lot of events itself. You need to handle touchesDidEnd and hit test for buttons inside the UIScrollView manually.

Roger Nolan
+10  A: 

In order for UIScrollView to determine the difference between a click that passes through to its content view(s) and a touch that turns into a swipe or pinch, it needs to delay the touch and see if your finger has moved during that delay. By setting delaysContentTouches to NO in your above example, you're preventing this from happening. Therefore, the scroll view is always passing the touch to the button, instead of canceling it when it turns out that the user is performing a swipe gesture. Try setting delaysContentTouches to YES.

It might also be a good idea, structurally, to add all the views to be hosted in your scroll view to a common content view and only use that one view as the scroll view's subview.

Brad Larson
A: 

Another way is:
1. Substitute de button by a simple custom UIView
2. Put the flag "userInterationEnable = yes;" on the init method
3. In the view override the UIResponder method "touchesEnded" here you can trigger the action you need like a button.

A: 

In my experience the first answer, i.e., simply setting delaysContentTouches to YES, does not change anything with regard to the problem. The buttons will still not deliver tracking results to the scroll view. The third answer is both simple and very usable. Thanks sieroaoj!

However, for the third answer to work you also need delaysContentTouches set to YES. Otherwise the method touchesEnded will also be called for tracking within the view. Therefore I could solve the problem by:

  1. Substitute de button by a simple custom UIView
  2. Put the flag "userInterationEnable = yes;" on the init method
  3. In the view override the UIResponder method "touchesEnded" here you can trigger the action you

Fourth. set delaysContentTouches to YES

Captain Fim
+1  A: 

OK here is your answer:

Subclass UIButton. (NOTE: call [super ....] at the start of each override.

  • Add a property. One of type BOOL (called enableToRestore)
  • Add a property. One of type CGPoint (called startTouchPosition)
  • in the awakeFromNib and initWithFrame, set the enableToRestore to the isEnabled property)
  • Override "touchesBegan: withEvent:" to store the start of the touch position.
  • Override "touchesMoved: withEvent:" to check to see if there is horizontal movement.
  • If YES, set enabled to NO and selected to NO.

Sample code:

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event 
{
    UITouch *touch = [touches anyObject];

    [super touchesBegan:touches withEvent:event];
    [self setStartTouchPosition:[touch locationInView:self]];
}


//
// Helper Function
//
- (BOOL)isTouchMovingHorizontally:(UITouch *)touch 
{
    CGPoint currentTouchPosition = [touch locationInView:self];
    BOOL      rValue = NO;

    if (fabsf([self startTouchPosition].x - currentTouchPosition.x) >= 2.0) 
    {
        rValue = YES;
    }

    return (rValue);
}

//
// This is called when the finger is moved.  If the result is a left or right
// movement, the button will disable resulting in the UIScrollView being the
// next responder.  The parrent ScrollView will then re-enable the button
// when the finger event is ended of cancelled.
//
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event 
{
    [super touchesMoved:touches withEvent:event];
    if ([self isTouchMovingHorizontally:[touches anyObject]]) 
    {
        [self setEnabled:NO];
        [self setSelected:NO];
    } 
}

This will activate the UIScrollView.

Subclass UIScrollView. (NOTE: call [super ....] at the start of each override.

  • Override both "touchesEnded: withEvent:" and "touchesCancelled: withEvent:"
  • In the override, reset all subviews (and their subviews) enabled flag.
  • NOTE: Use a Category and add the method to UIView:

.

- (void) restoreAllEnables
{
    NSArray   *views = [self subviews];

    for (UIView *aView in views)
    {
        if ([aView respondsToSelector:@selector(restoreEnable)])
        {
            [aView restoreEnable];
        }
    }
}

- (void) touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
    [super touchesEnded:touches withEvent:event];
    [self restoreAllEnables];
}

- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event
{
    [super touchesEnded:touches withEvent:event];
    [self restoreAllEnables];
}
  • In the Category:

.

-(void) restoreEnable
{
    NSArray   *views = [self subviews];

    if ([self respondsToSelector:@selector(enableToRestore)])
    {
        [self setEnabled:[self enableToRestore]];
    }

    for (UIView *aView in views)
    {
        if ([aView respondsToSelector:@selector(restoreEnable)])
        {
            [aView restoreEnable];
        }
    }
}

EDIT Note: I never got Answer 3 to work. Likewise: the setDelaysContentTouches:NO (set in the view controller or someplace) is to be set for best results on Answer 4. This provides very fast response to the buttons. Setting setDelaysContentTouches:YES puts a serious impact (150ms) on response time to the buttons and makes light,fast touching not possible.

Steven Noyes
A: 

I have a similar case that a number of buttons on a UIScrollView, and I want to scroll these buttons. At the beginning, I subclassed both UIScrollView and UIButton. However, I noticed that my subclassed UIScrollView did not receive touchesEnded event, so I changed to only subclass UIButton.


@interface MyPhotoButton : UIButton {
}

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event;
@end

@implementation MyPhotoButton

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
    [super touchesMoved:touches withEvent:event];
    [self setEnabled:NO];
    [self setSelected:NO];
}

- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
    [super touchesEnded:touches withEvent:event];
    [self setEnabled:YES];
}

- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
    [super touchesCancelled:touches withEvent:event];
    [self setEnabled:YES];
}

@end
Richard Chen
A: 

Solution that worked for me included:

  1. Setting canCancelContentTouches in UIScrollView to YES.
  2. Extending UIScrollView to override touchesShouldCancelInContentView:(UIView *)view to return YES when view is a UIButton.

According to documentation touchesShouldCancelInContentView returns "YES to cancel further touch messages to view, NO to have view continue to receive those messages. The default returned value is YES if view is not a UIControl object; otherwise, it returns NO."

Since UIButton is a UIControl the extension is necessary to get canCancelContentTouches to take effect which enables scrolling.

Roman K