71

I've looked at a ton of posts on similar things, but none of them quite match or fix this issue. Since iOS 7, whenever I add a UIButton to a UITableViewCell or even to the footerview it works "fine", meaning it receives the target action, but it doesn't show the little highlight that normally happens as you tap a UIButton. It makes the UI look funky not showing the button react to touch.

I'm pretty sure this counts as a bug in iOS7, but has anyone found a solution or could help me find one :)

Edit: I forgot to mention that it will highlight if I long hold on the button, but not a quick tap like it does if just added to a standard view.

Code:

Creating the button:

UIButton *button = [UIButton buttonWithType:UIButtonTypeRoundedRect];
    button.titleLabel.font = [UIFont systemFontOfSize:14];
    button.titleLabel.textColor = [UIColor blueColor];
    [button setTitle:@"Testing" forState:UIControlStateNormal];
    [button addTarget:self action:@selector(buttonPressed:) forControlEvents: UIControlEventTouchDown];
    button.frame = CGRectMake(0, 0, self.view.frame.size.width/2, 40);

Things I've Tested:

//Removing gesture recognizers on UITableView in case they were getting in the way.

for (UIGestureRecognizer *recognizer in self.tableView.gestureRecognizers) {
   recognizer.enabled = NO;
}

//Removing gestures from the Cell

for (UIGestureRecognizer *recognizer in self.contentView.gestureRecognizers) {
       recognizer.enabled = NO;
    }

//This shows the little light touch, but this isn't the desired look

button.showsTouchWhenHighlighted = YES;
Silviu St
  • 1,810
  • 3
  • 33
  • 42
Eric
  • 5,671
  • 5
  • 31
  • 42
  • Use setTitleColor for highlighted state. (not sure but also try UIButtonTypeCustom ) – msk Oct 08 '13 at 20:00
  • 1
    That doesn't fix the problem. If I long hold on the button it will change to the highlighted color, but not just on a single tap – Eric Oct 08 '13 at 20:46
  • Having this problem with a subclassed UIButton that's just in a regular ViewController, not under a scrollview or table. Any ideas? – Ben Wheeler Jul 03 '14 at 21:14

17 Answers17

91

In that tableview you just add this property.

tableview.delaysContentTouches = NO;

And add in cellForRowAtIndexPath after you initiate the cell you just add below code. The structure of the cell is apparently different in iOS 6 and iOS 7.
iOS 7 we have one control UITableViewCellScrollView In between UITableViewCell and content View.

for (id obj in cell.subviews)
{
    if ([NSStringFromClass([obj class]) isEqualToString:@"UITableViewCellScrollView"])
    {
        UIScrollView *scroll = (UIScrollView *) obj;
        scroll.delaysContentTouches = NO;
        break;
    }
}
Quentin
  • 3,971
  • 2
  • 26
  • 29
subramani
  • 942
  • 7
  • 5
  • 3
    You are a genius, thank you! I edited your subview loop to make it a little more future proof. Posted the edit, so once it gets approved will hopefully help others out – Eric Oct 11 '13 at 00:56
  • sure,thanks for posting this. I hope this will be helpful for others also. – subramani Oct 12 '13 at 14:29
  • Thanks man. I tried to write the condition `([obj isKindOfClass:[UITableViewCellScrollView class]])` but it raises an error saying that it doesn't know about a class named `UITableViewCellScrollView`. Why? – Fred Collins Nov 13 '13 at 18:35
  • Brilliant, been struggling with this one for a while now. – Ryan Romanchuk Nov 19 '13 at 17:22
  • @subramani i just noticed in one of my tableviews scrolling has stopped, i need to investigate why, but the only difference is that this cell has multiple buttons. Any ideas what could be happening? @Eric? – Ryan Romanchuk Nov 20 '13 at 17:20
  • @subramani actually I think it's related to having multiple scrollviews – Ryan Romanchuk Nov 20 '13 at 17:26
  • 2
    @RyanRomanchuk I've used it with multiple buttons. I know that this does stop scrolling if the start of the scroll (where you tap down) is a button. It's because now the touch gets sent to the button and not the scroll view. I still do this because I think showing the button highlight is more important than making the scroll work even if they start the scroll on the button. That may just be in my case though – Eric Nov 23 '13 at 08:55
  • @Eric strange..my entire scrollview doesn't scroll anymore (regardless of where I'm tapping), but only on one specific controller. The only difference with this view controller is that it has multiple scrollviews. – Ryan Romanchuk Nov 24 '13 at 17:33
  • @RyanRomanchuk Are you nesting scrollviews, or are there just multiple side by side. If you are nesting, maybe touches aren't being passed to the subscrollViews – Eric Nov 25 '13 at 17:51
  • @Eric there is a scrollview positioned above the uitableview acting as a photo browser/parallax, i haven't investigated much, seems bizarre that it's causing problems, but it's the only variable i can think of at the moment – Ryan Romanchuk Nov 25 '13 at 20:48
  • @subramani You are my today's favorite person :) Thank you! BTW, sadly SO is full of other *wrong* answers to the same question... – sonxurxo Dec 13 '13 at 18:16
  • If you are nesting scrollviews you might want to subclass the scrollview and override the built in check for UIControl. - (BOOL)touchesShouldCancelInContentView:(UIView *)view { if ([self isRunningiOS7OrLater]) { return YES; } return [super touchesShouldCancelInContentView:view]; } – Marc Etcheverry Feb 23 '14 at 01:28
  • This answer also helped me fix a bug with a regular UIScrollView. Thanks! – bmeulmeester Jul 31 '14 at 06:36
  • 5
    Unfortunately this code will not work in iOS8 as the UITableViewCellScrollView has been removed from the UITableViewCell view hierarchy. – Quentin Sep 25 '14 at 21:54
  • See my answer: http://stackoverflow.com/a/26049216/194191 for a solution that works in iOS7 and iOS8 – Quentin Sep 26 '14 at 00:11
39

Since iOS 8 we need to apply the same technique to UITableView subviews (table contains a hidden UITableViewWrapperView scroll view). There is no need iterate UITableViewCell subviews anymore.

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

This answer should be linked with this question.

Community
  • 1
  • 1
Roman B.
  • 3,598
  • 1
  • 25
  • 21
  • Thanks! It works, but it's a weird solution. I can't believe Apple didn't take care of this behaviour without inspecting of subviews. – surfrider Nov 02 '15 at 18:34
  • 1
    Faced the same issue on iOS11. There only tableView.delaysContentTouches = NO was needed to fix the problem. Looks like they FINALLY fixed that (it seems to be present on iOS10 though) – RadioLog Aug 10 '18 at 12:27
  • Im facing this in iOS14 – iadcialim24 Sep 28 '20 at 15:08
27

I tried to add this to the accepted answer but it never went through. This is a much safer way of turning off the cells delaysContentTouches property as it does not look for a specific class, but rather anything that responds to the selector.

In Cell:

for (id obj in self.subviews) {
     if ([obj respondsToSelector:@selector(setDelaysContentTouches:)]) {
          [obj setDelaysContentTouches:NO];
     }
}

In TableView:

self.tableView.delaysContentTouches = NO;
Eric
  • 5,671
  • 5
  • 31
  • 42
  • 3
    This is better than the accepted answer. One point I would add is that it's better to add this right before the cell is returned rather than after it's instantiated because setting up the cell can reset `setDelaysContentTouches` – Mika Mar 12 '14 at 11:41
  • 1
    +1 Mikael, that made the difference for me. +1 for the answer because `respondsToSelector` is a much better solution than class comparison. – user Apr 29 '14 at 08:15
18

For a solution that works in both iOS7 and iOS8, create a custom UITableView subclass and custom UITableViewCell subclass.

Use this sample UITableView's initWithFrame:

- (id)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];

    if (self)
    {
        // iterate over all the UITableView's subviews
        for (id view in self.subviews)
        {
            // looking for a UITableViewWrapperView
            if ([NSStringFromClass([view class]) isEqualToString:@"UITableViewWrapperView"])
            {
                // this test is necessary for safety and because a "UITableViewWrapperView" is NOT a UIScrollView in iOS7
                if([view isKindOfClass:[UIScrollView class]])
                {
                    // turn OFF delaysContentTouches in the hidden subview
                    UIScrollView *scroll = (UIScrollView *) view;
                    scroll.delaysContentTouches = NO;
                }
                break;
            }
        }
    }
    return self;
}

Use this sample UITableViewCell's initWithStyle:reuseIdentifier:

- (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier 
{
    self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];

    if (self)
    {
        // iterate over all the UITableViewCell's subviews
        for (id view in self.subviews)
        {
            // looking for a UITableViewCellScrollView
            if ([NSStringFromClass([view class]) isEqualToString:@"UITableViewCellScrollView"])
            {
                // this test is here for safety only, also there is no UITableViewCellScrollView in iOS8
                if([view isKindOfClass:[UIScrollView class]])
                {
                    // turn OFF delaysContentTouches in the hidden subview
                    UIScrollView *scroll = (UIScrollView *) view;
                    scroll.delaysContentTouches = NO;
                }
                break;
            }
        }
    }

    return self;
}
Quentin
  • 3,971
  • 2
  • 26
  • 29
  • How to write the function `initWithFrame`? PriceDetailTableView.m:66:18: No visible @interface for 'UITableViewController' declares the selector 'initWithFrame:'. My code:`- (id) initWithFrame:(CGRect)frame style:(UITableViewStyle)style { self = [super initWithFrame:frame ]; self = [super initWithStyle:style]; return self; }` – Gank Dec 11 '14 at 06:47
  • `- (id) initWithFrame:(CGRect)frame style:(UITableViewStyle)style { self=[super initWithFrame:frame style:style]; return self; }`PriceDetailTableView.m:80:17: No visible @interface for 'UITableViewController' declares the selector 'initWithFrame:style:' – Gank Dec 11 '14 at 07:04
  • and UITableViewCell's initWithFrame will not enter in – Gank Dec 11 '14 at 07:15
  • 1
    @Gank: I updated the post for a little more clarity. You need to implement `- (id)initWithFrame:(CGRect)frame` in your _PriceDetailTableView_ and `- (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier` in your _PriceDetailTableViewCell_. Does that make more sense? – Quentin Dec 15 '14 at 20:23
  • How to write it in Swift? I cannot do self = super.init(style: style, reuseIdentifier: reuseIdentifier) in Swift: Cannot assign to 'self' in a method. Also, view.class is not working – thomasdao May 11 '15 at 11:23
  • Write content of If (self) {} in awakeFromNib If you are using storyboard in both objective C and swift – Abhishek Jun 02 '15 at 06:05
17

What I did to solve the problem was a category of UIButton using the following code :

- (void) touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    [super touchesBegan:touches withEvent:event];


    [NSOperationQueue.mainQueue addOperationWithBlock:^{ self.highlighted = YES; }];
}


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

    [self performSelector:@selector(setDefault) withObject:nil afterDelay:0.1];
}

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

    [self performSelector:@selector(setDefault) withObject:nil afterDelay:0.1];
}


- (void)setDefault
{
    [NSOperationQueue.mainQueue addOperationWithBlock:^{ self.highlighted = NO; }];
}

the button reacts correctly when I press on it in a UITableViewCell, and my UITableView behaves normally as the delaysContentTouches isn't forced.

Raphaël Pinto
  • 653
  • 8
  • 20
  • 2
    Category? You sure? :) – Rusik Feb 24 '15 at 21:27
  • 1
    Works as a charm to put in a subclass of UIButton. Much love <3 – Nailer Apr 17 '15 at 10:58
  • 1
    Good solution, particularly if we are using UITableView on another view, not in a UIViewController (that case, delaysContentTouches doesn't work). – lenhhoxung May 12 '15 at 07:56
  • 1
    This is a great solution. All i found was examples based on view being a subclass of UIScrollView or TableView. I did not have anyof them as a parent class. Nothing else worked. this is great. – Evren Bingøl Jul 27 '15 at 05:44
11

Here's Roman B's answer in Swift 2:

for view in tableView.subviews {
    if view is UIScrollView {
        (view as? UIScrollView)!.delaysContentTouches = false
        break
    }
}
brandonscript
  • 68,675
  • 32
  • 163
  • 220
  • 3
    Not sure why no one has tried this, but has worked for me. Question is why would the tableview, which is a scrollview subclass itself, have another scrollview inside of it. Apple :| – pnizzle Feb 22 '16 at 23:43
  • 1
    Why is this gem hidden here in the bottom? – Leverin Mar 24 '16 at 14:33
5
    - (void)viewDidLoad
{

    [super viewDidLoad];


    for (id view in self.tableView.subviews)
    {
        // looking for a UITableViewWrapperView
        if ([NSStringFromClass([view class]) isEqualToString:@"UITableViewWrapperView"])
        {
            // this test is necessary for safety and because a "UITableViewWrapperView" is NOT a UIScrollView in iOS7
            if([view isKindOfClass:[UIScrollView class]])
            {
                // turn OFF delaysContentTouches in the hidden subview
                UIScrollView *scroll = (UIScrollView *) view;
                scroll.delaysContentTouches = NO;
            }
            break;
        }
    }
}

enter image description here

Gank
  • 4,507
  • 4
  • 49
  • 45
  • Actually, this is the cleanest answer on the page IMO and works as well in ios 7 as any of the others, and works properly in ios 8. That is, I'm getting ios 7 to only show the flash when the cell is selected as with all the other answers. Am I missing something? – SmileBot Mar 09 '15 at 00:28
  • Ok, I got it, I see that you have to give the button a background color in ios 7 to get the flash but not in ios 8. Got it. Great answer!! Thanx. – SmileBot Mar 09 '15 at 00:35
  • True solution for storyboard and ios 8 and above – lenhhoxung May 12 '15 at 07:02
5

I was having similar issues with a text-only UIButton in a UITableViewCell not highlighting upon touch. What fixed it for me was changing the buttonType from Custom back to System.

Setting delaysContentTouches to NO did the trick for the image-only UIButton in the same UITableViewCell.

self.tableView.delaysContentTouches = NO;

enter image description here

skg
  • 133
  • 3
  • 6
4

This is a Swift version of Raphaël Pinto's answer above. Don't forget to upvote him too :)

override func touchesBegan(touches: NSSet, withEvent event: UIEvent) {
    super.touchesBegan(touches, withEvent: event)
    NSOperationQueue.mainQueue().addOperationWithBlock { () -> Void in self.highlighted = true }
}

override func touchesCancelled(touches: NSSet!, withEvent event: UIEvent!) {
    super.touchesCancelled(touches, withEvent: event)
    let time = dispatch_time(DISPATCH_TIME_NOW, Int64(0.1 * Double(NSEC_PER_SEC)))
    dispatch_after(time, dispatch_get_main_queue()) {
        self.setDefault()
    }
}

override func touchesEnded(touches: NSSet, withEvent event: UIEvent) {
    super.touchesEnded(touches, withEvent: event)
    let time = dispatch_time(DISPATCH_TIME_NOW, Int64(0.1 * Double(NSEC_PER_SEC)))
    dispatch_after(time, dispatch_get_main_queue()) {
        self.setDefault()
    }
}

func setDefault() {
    NSOperationQueue.mainQueue().addOperationWithBlock { () -> Void in self.highlighted = false }
}
Chris Harrison
  • 5,512
  • 3
  • 28
  • 36
  • 1
    That solution works fine if you don't use the button selected state. The exact parameters for swift 2 are "touches: Set, withEvent event: UIEvent?". – antoine Apr 12 '16 at 21:51
3

Solution in Swift, iOS8 only (needs the extra work on each of the cells for iOS7):

//
//  NoDelayTableView.swift
//  DivineBiblePhone
//
//  Created by Chris Hulbert on 30/03/2015.
//  Copyright (c) 2015 Chris Hulbert. All rights reserved.
//
//  This solves the delayed-tap issue on buttons on cells.

import UIKit

class NoDelayTableView: UITableView {

    required init(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)

        delaysContentTouches = false

        // This solves the iOS8 delayed-tap issue.
        // http://stackoverflow.com/questions/19256996/uibutton-not-showing-highlight-on-tap-in-ios7
        for view in subviews {
            if let scroll = view as? UIScrollView {
                scroll.delaysContentTouches = false
            }
        }
    }

    override func touchesShouldCancelInContentView(view: UIView!) -> Bool {
        // So that if you tap and drag, it cancels the tap.
        return true
    }

}

To use, all you have to do is change the class to NoDelayTableView in your storyboard.

I can confirm that in iOS8, buttons placed inside a contentView in a cell now highlight instantly.

Chris
  • 39,719
  • 45
  • 189
  • 235
2

Slightly modified version of Chris Harrison's answer. Swift 2.3:

class HighlightButton: UIButton {
    override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
        super.touchesBegan(touches, withEvent: event)
        NSOperationQueue.mainQueue().addOperationWithBlock { _ in self.highlighted = true }
    }

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

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

    private func setDefault() {
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, Int64(0.1 * Double(NSEC_PER_SEC))), dispatch_get_main_queue()) {
            NSOperationQueue.mainQueue().addOperationWithBlock { _ in self.highlighted = false }
        }
    }
}
Community
  • 1
  • 1
SoftDesigner
  • 5,640
  • 3
  • 58
  • 47
1

The accepted answer did not work at some "taps" for me .

Finally I add the bellow code in a uibutton category(/subclass),and it works a hundred percent.

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{

self.backgroundColor = [UIColor greenColor];
[UIView animateWithDuration:0.05 delay:0 options:UIViewAnimationOptionCurveLinear animations:^{
    self.backgroundColor = [UIColor clearColor];

} completion:^(BOOL finished)
 {
 }];
[super touchesBegan:touches withEvent:event];

}
Rahil Wazir
  • 10,007
  • 11
  • 42
  • 64
rotoava
  • 625
  • 8
  • 18
0

I wrote a category extension on UITableViewCell to make this issue simple to address. It does basically the same thing as the accepted answer except I walk up the view hierarchy (as opposed to down) from the UITableViewCell contentView.

I considered a fully "automagic" solution that would make all cells added to a UITableView set their delaysContentTouches state to match the owning UITableView's delaysContentTouches state. To make this work I'd have to either swizzle UITableView, or require the developer to use a UITableView subclass. Not wanting to require either I settled on this solution which I feel is simpler and more flexible.

Category extension and sample harness here:

https://github.com/TomSwift/UITableViewCell-TS_delaysContentTouches

It's dead-simple to use:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    // using static cells from storyboard...
    UITableViewCell* cell = [super tableView: tableView cellForRowAtIndexPath: indexPath];

    cell.ts_delaysContentTouches = NO;

    cell.selectionStyle = UITableViewCellSelectionStyleNone;

    return cell;
}

Here's the code for the category:

@interface UITableViewCell (TS_delaysContentTouches)

@property (nonatomic, assign) BOOL ts_delaysContentTouches;

@end

@implementation UITableViewCell (TS_delaysContentTouches)

- (UIScrollView*) ts_scrollView
{
    id sv = self.contentView.superview;
    while ( ![sv isKindOfClass: [UIScrollView class]] && sv != self )
    {
        sv = [sv superview];
    }

    return sv == self ? nil : sv;
}

- (void) setTs_delaysContentTouches:(BOOL)delaysContentTouches
{
    [self willChangeValueForKey: @"ts_delaysContentTouches"];

    [[self ts_scrollView] setDelaysContentTouches: delaysContentTouches];

    [self didChangeValueForKey: @"ts_delaysContentTouches"];
}

- (BOOL) ts_delaysContentTouches
{
    return [[self ts_scrollView] delaysContentTouches];
}

@end
TomSwift
  • 39,369
  • 12
  • 121
  • 149
  • So basically setting one cells ts_delaysContentTouches sets the entire tableViews delaysContentTouches correct? So this wouldn't allow dynamic cell setting, but rather setting the whole tableview as a whole? – Eric Jan 31 '14 at 20:33
  • @Eric - no. I consciously decided not to also have it set the tableview delaysContentTouches. I feel that is a side effect that is unexpected since it would be changing a view upstream in the hierarchy. You still need to set tableView.delaysContentTouches = NO. – TomSwift Jan 31 '14 at 20:44
0

Since objc is dynamic, and scrollView is the only class that responds to delaysContentTouches, this should work for both ios 7 and 8 (put it somewhere early in your tableViewController, like awakeFromNib):

for (id view in self.tableView.subviews)
    {
        if ([view respondsToSelector:@selector(delaysContentTouches)]) {
            UIScrollView *scrollView = (UIScrollView *)view;
            scrollView.delaysContentTouches = NO;
            break;
        }
}

You may also have to turn off "delaysContentTouches" in your storyboard or nib by selecting the table inside your viewController. BTW, this might not work on ios 7 if you're using a tableView inside a viewController, at least I couldn't get it to work.

SmileBot
  • 19,393
  • 7
  • 65
  • 62
0

That solution for me doesn't work, I fixed subclassing TableView and implementing these two methods

- (instancetype)initWithCoder:(NSCoder *)coder{
   self = [super initWithCoder:coder];
   if (self) {
     for (id obj in self.subviews) {
      if ([obj respondsToSelector:@selector(setDelaysContentTouches:)]){
            [obj performSelector:@selector(setDelaysContentTouches:) withObject:NO];
      }
     }
   }
   return self;
}

- (BOOL)delaysContentTouches{
   return NO;
}
Serluca
  • 2,102
  • 2
  • 19
  • 31
0

Solution in Swift for iOS 7 and 8:

First I wrote a utility function:

class func classNameAsString(obj: AnyObject) -> String {
    return _stdlib_getDemangledTypeName(obj).componentsSeparatedByString(".").last!
}

then I subclass UITableView and implement this:

required init(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)

    for view in self.subviews {
        if (Utility.classNameAsString(view) == "UITableViewWrapperView") {
            if view.isKindOfClass(UIScrollView) {
                var scroll = (view as UIScrollView)
                scroll.delaysContentTouches = false
            }
            break
        }
    }
}

I also subclass UITableViewCell and implement this:

required init(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)

    for view in self.subviews {
        if (Utility.classNameAsString(view) == "UITableViewCellScrollView") {
            if view.isKindOfClass(UIScrollView) {
                var scroll = (view as UIScrollView)
                scroll.delaysContentTouches = false
            }

        }
    }
}

In my case the init(coder:) will run. Please put debug point in your init functions to know which init function will run, then using the code above to make it work. Hope to help someone.

thomasdao
  • 972
  • 12
  • 26
0

In Swift 3 this UIView extension can be used on the UITableViewCell. Preferably in the cellForRowAt method.

func removeTouchDelayForSubviews() {
    for subview in subviews {
        if let scrollView = subview as? UIScrollView {
            scrollView.delaysContentTouches = false
        } else {
            subview.removeTouchDelayForSubviews()
        }
    }
}
Sunkas
  • 9,542
  • 6
  • 62
  • 102