45

I have a custom UITableViewCell, which is initialized by the following:

- (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier
{
    self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
    if (self) {
        NSArray *nibArray = [[NSBundle mainBundle] loadNibNamed:@"CustomCell" owner:self options:nil];
        self = [nibArray objectAtIndex:0];

        [self setSelectionStyle:UITableViewCellSelectionStyleNone];

        [self.downButton setBackgroundImage:[UIImage imageNamed:@"button"] forState:UIControlStateNormal];
        [self.downButton setBackgroundImage:[UIImage imageNamed:@"buttonSelected"] forState:UIControlStateHighlighted];
    }
    return self;
}

The button appears properly, with the appropriate background image, but the highlighted image does not instantly appear when the button is pressed/clicked. Instead, you have to hold it down for a second or two before the change occurs. Releasing the button, on the other hand, does have an instant change back to the original background image.

Trying to mitigate the tardy change when switching to the highlighted image, I put the change in the following method:

- (IBAction)downDown:(id)sender {
    [self.downButton setBackgroundColor:[UIColor redColor]];
}

The method above is set for "Touch Down" (opposed to the more common "Touch Up Inside"), and I have removed the setBackgroundImage:forState: for the highlighted state. Same problem as mentioned above. The color does eventually change to red, but only after clicking and holding on the button for a second or two.

I have a method for the button that is called when "Touch Up Inside" occurs, and that method executes without issue - regardless of whether I quickly tap the button, or click and hold on it for a length of time before releasing.

So why the delay for the "Touch Down" or UIControlStateHighlighted? I'm trying to provide instant feedback to the user to show that the button has been pressed.

I can provide more code if needed, but these are the only bits that have anything to do with the background appearance.

Birrel
  • 4,754
  • 6
  • 38
  • 74
  • I want to say it's because the images aren't loaded beforehand, but obviously this wouldn't be applicable in the color case. Is it different when you don't create your own and use the Xcode's storyboard to generate the button? – Shahar Apr 07 '14 at 23:21
  • Same deal. If I define the background images/colors in the storyboard editor, the button still requires the user to hold it down for a period of time before any change occurs. – Birrel Apr 07 '14 at 23:27
  • That's weird behavior. Since nobody answered the question, I suggest Googling something along the line of "objective c touchdown delay." – Shahar Apr 07 '14 at 23:35
  • That was the first place I looked. I wouldn't post something on here without fighting with it for quite some time first. – Birrel Apr 08 '14 at 01:49
  • It doesn't work for me on iOS 9 – Nadzeya May 06 '16 at 13:22

7 Answers7

90

This is caused by the UIScrollView property delaysContentTouches.

It used to be sufficient to just set that property to NO for the UITableView itself, but that will only work for subviews of the table that are not encased in another UIScrollView.

UITableViewCells contain an internal scroll view in iOS 7 so you will need to change the value of this property on the cell level for all cells with buttons in them.

Here is what you need to do:

1.in viewDidLoad or somewhere similar once your UITableView has been initialized, put this in:

self.tableView.delaysContentTouches = NO;

2.for iOS 7 support, in the initialization method for your UITableViewCell (initWithStyle:reuseIdentifier: or initWithCoder: for NIBs), put this in at the end:

for (UIView *currentView in self.subviews)
{
    if([currentView isKindOfClass:[UIScrollView class]])
    {
        ((UIScrollView *)currentView).delaysContentTouches = NO;
        break;
    }
}

This is unfortunately not a 100% permanent solution as Apple can change the view hierarchy inside cells again in the future (perhaps moving the scroll view another layer down or something which would require you to nest another loop in there), but until they surface the class or at least the property to developers somehow, this is the best we've got.

Dima
  • 23,484
  • 6
  • 56
  • 83
  • 3
    There's no need to know the exact (private) class name of the cell's scroll view, just use `if ([currentView isKindOfClass:[UIScrollView class]]) {...}`, this will be true for subclasses of `UIScrollView` as well. – omz Apr 08 '14 at 00:53
  • 2
    Perfect! I've been battling this for quite some time now. How does a person find out such specific solutions to these issues!? – Birrel Apr 08 '14 at 01:48
  • 1
    This solved a problem I had noticed ut hadn't sat down figure out the solution yet. Thanks – Tim Apr 11 '14 at 12:39
  • what is the "initialization method for your `UITableViewCell`"? – Adam Johns Aug 13 '14 at 18:33
  • @AdamJohns ultimately it would be `initWithStyle:reuseIdentifier: ` – Dima Aug 13 '14 at 18:48
  • 1
    Oh I see. My `initWithStyle` method isn't called because my cell is created in IB. I was able to use `initWithCoder` as [this answer](http://stackoverflow.com/a/8522967/1438339) mentions. – Adam Johns Aug 13 '14 at 19:05
  • 1
    @AdamJohns added that note to my answer for everyone else's benefit, thanks. – Dima Aug 13 '14 at 19:09
  • 1
    As @Dima feared, the view hierarchy inside a UITableViewCell has changed and unfortunately this solution does not work in iOS8. The UITableViewCellScrollView that used to be in the hierarchy in iOS7, is missing in iOS8. My guess is that the UITableViewCellScrollView functionality has been rolled in to either the UITableViewCellContentView or the UITableViewCell itself? – Quentin Sep 25 '14 at 21:15
  • See this answer:http://stackoverflow.com/a/26049216/194191 for a solution that works in both iOS7 and iOS8. – Quentin Sep 26 '14 at 00:04
  • After my cell was added in the view hierarchy I had to setDelaysContentTouches to NO on its superview, a UITableViewWrapperView, which is in between the table view and the cells. – Andrei Marincas Feb 23 '15 at 14:38
  • Supposedly one of the subviews of a UITableView is a UITableViewWrapperView which is also a UIScrollView. Setting delayedContentTouches to NO on that view solved the issue for me. – Werner Altewischer Mar 13 '17 at 10:21
  • Dima dawg/cat, this problem persists to this day on [IOS less than 11]. On IOS 11+, if you specify tableView.delaysContentTouches = true, it works fine, but if it's IOS 10 or less, it still delays... can you edit your answer to include Swift? I tried using the swiftify website but it gave me code that causes a sweet sweet infinite loop. – Nerdy Bunz Dec 21 '18 at 04:26
  • I just checked the code swiftify spits out and it looks fine to me – Dima Jun 03 '19 at 15:20
28

Swift:

Firstly, turn off delays in UITableView after it is loaded successfully, for example, inside viewDidLoad() method:

someTableView.delaysContentTouches = false

Then turn off delays in the scroll views contained inside the UITableView:

for case let scrollView as UIScrollView in someTableView.subviews {
    scrollView.delaysContentTouches = false
}

Note for iOS7: You might have to disable the delays in UITableViewCell. (Check Dima's answer). You might also find some other tips here.

Nitin Nain
  • 5,113
  • 1
  • 37
  • 51
20

i solve this problem in my code by subclassing UIButton,

Objective C

@interface TBButton : UIButton

@end

@implementation TBButton

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    self.highlighted = true;
    [super touchesBegan:touches withEvent:event];
}

- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    self.highlighted = false;
    [super touchesEnded:touches withEvent:event];
}

- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    self.highlighted = false;
    [super touchesCancelled:touches withEvent:event];
}

@end

swift

class TBButton: UIButton {
    override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
        highlighted = true
        super.touchesBegan(touches, withEvent: event)
    }

    override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) {
        highlighted = false
        super.touchesEnded(touches, withEvent: event)
    }
    override func touchesCancelled(touches: Set<UITouch>?, withEvent event: UIEvent?) {
        highlighted = false
        super.touchesCancelled(touches, withEvent: event)
    }
}

swift 3

class TBButton: UIButton {
    override func touchesBegan(_ touches: Set<UITouch>, with event:     UIEvent?) {
        isHighlighted = true
        super.touchesBegan(touches, with: event)
    }

    override func touchesEnded(_ touches: Set<UITouch>, with event:     UIEvent?) {
        isHighlighted = false
        super.touchesEnded(touches, with: event)
    }

    override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
        isHighlighted = false
        super.touchesCancelled(touches, with: event)
    }
}
Alex
  • 1,603
  • 1
  • 16
  • 11
4

Swift 3 version of Alex's answer:

override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
    isHighlighted = true
    super.touchesBegan(touches, with: event)
}

override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
    isHighlighted = false
    super.touchesEnded(touches, with: event)
}

override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
    isHighlighted = false
    super.touchesCancelled(touches, with: event)
}
Community
  • 1
  • 1
Marc
  • 609
  • 1
  • 5
  • 10
2

Here's a recursive solution you can add to your view controller:

+ (void)disableDelayedTouches:(UIView*)view
{
    for(UIView *currentView in view.subviews) {
        if([currentView isKindOfClass:[UIScrollView class]]) {
            ((UIScrollView *)currentView).delaysContentTouches = NO;
        }
        [self disableDelayedTouches:currentView];
    }
}

- (void)viewDidLoad
{
    [super viewDidLoad];
    [self.class disableDelayedTouches:self.view];
}
Sandy Chapman
  • 11,133
  • 3
  • 58
  • 67
2

So why the delay for the "Touch Down" or UIControlStateHighlighted? I'm trying to provide instant feedback to the user to show that the button has been pressed.

Hasn't Apple explained it yet?

Without this delay:

  1. If you want to scroll the view, and your finger touches anything highlightable (e.g. any table cell with the default selection style), you get these blinking artifacts:

    enter image description here

    Really, they don't know on touchDown, whether you're going to tap or scroll.

  2. If you want to scroll the view, and your finger touches a UIButton/UITextField, the view isn't scrolled at all! Button/TextField's default behavour is "stronger" than the scrollview's, it keeps waiting for a touchUpInside event.

Dmitry Isaev
  • 3,888
  • 2
  • 37
  • 49
-1

Maybe this answer is more simple. just add an animation delay to it. like below:

    [self.publishButton bk_addEventHandler:^(id sender) {
    @strongify(self);
    // reset to normal state with delay
    [UIView animateWithDuration:0.1 animations:^{
        self.publishButton.backgroundColor = [UIColor whiteColor];
    }];
    } forControlEvents:UIControlEventTouchUpInside];

   [self.publishButton bk_addEventHandler:^(id sender) {
    //Set your Image or color
       self.publishButton.backgroundColor = [UIColor redColor];
   } forControlEvents:UIControlEventTouchDown];
Jonguo
  • 681
  • 1
  • 10
  • 17