34

Brief

I am having an issue with a UITableView inside a UIScrollView. When I scroll the external scrollView, the table does not receive the willSelect/didSelect event on the first touch, but it does on the second one. What is even more strange, the cell itself gets the touches and the highlighted state, even when the delegate does not.

Detailed explanation

My view hierarchy:

UIView
  - UIScrollView  (outerscroll)
      - Some other views and buttons
      - UITableView (tableView)

Inside the scroll view I have some extra views that get expanded/closed dynamically. The table view needs to get "fixed" on top, together with some other elements of the view, so that is why I created this layout, that allows me to easily move elements in a similar way than Apple recommends by the use of transformations when the scroll happens.

The table View is transformed with a translation effect when the outerscroll moves like this:

- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
    if (scrollView == self.outerScrollView) {

        CGFloat tableOffset = scrollView.contentOffset.y - self.fixedHeaderFrame.origin.y;
        if (tableOffset > 0) {
            self.tableView.contentOffset = CGPointMake(0, tableOffset);
            self.tableView.transform = CGAffineTransformMakeTranslation(0, tableOffset);
        }
        else {
            self.tableView.contentOffset = CGPointMake(0, 0);
            self.tableView.transform = CGAffineTransformIdentity;
        }

        // Other similar transformations are done here, but not involving the table

}

In my cell, if I implement these methods:

- (void)setSelected:(BOOL)selected {
    [super setSelected:selected];
    if (selected) {
        NSLog(@"selected");
    }
}

- (void)setHighlighted:(BOOL)highlighted animated:(BOOL)animated
{
    [super setHighlighted:highlighted animated:animated];
    if (highlighted) {
        NSLog(@"highlighted");
    }
}

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

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

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

Y can see this output when fails (first tap):

2014-02-10 13:04:40.940 MyOrderApp[5588:70b] highlighted 
2014-02-10 13:04:40.940 MyOrderApp[5588:70b] touchesBegan 
2014-02-10 13:04:40.978 MyOrderApp[5588:70b] touchesEnded

And this one when works (second tap):

2014-02-10 13:05:30.359 MyOrderApp[5588:70b] highlighted 
2014-02-10 13:05:30.360 MyOrderApp[5588:70b] touchesBegan 
2014-02-10 13:05:30.487 MyOrderApp[5588:70b] touchesEnded 
2014-02-10 13:05:30.498 MyOrderApp[5588:70b] expanded

No other frame change, animation or any other view interaction is done between the first and the second tap. Also, only when scrolling large amounts the bug appears, but with scrollings of just a few pixels everything keeps working as expected.

I experimented changing some properties as well, but with no luck. Some of the things I did:

  • Remove userInteractionEnabled from views other than the scroll and table
  • Add a call to setNeedsLayout on the table, scroll and main view when scrollViewDidScroll occurs.
  • Remove the transformations from the table (still happens)

I have seen some comments about the unexpected behaviour of embedding UITableViews inside UIScrollViews but I can not see such a warn in the official documentation by Apple, so I am expecting it to work.

The app is iOS7+ only.

Questions

Has anyone experienced similar issues? Why is this and how can I solve it? I think that I could be able to intercept the tap gesture on the cell and pass it with a custom delegate or similar, but I would like the table to receive the proper events and so my UITableViewDelegate receives it as expected.

Updates

  • I tried disabling cell reuse as suggested in a comment but it still happens in the same way.
Rajesh
  • 850
  • 9
  • 18
Angel G. Olloqui
  • 8,045
  • 3
  • 33
  • 31
  • 1
    have you tried [yourScroll bringSubviewToFront: yourTable]; ? – Ilario Feb 12 '14 at 15:08
  • 1
    You mention that this only happens when you scroll a lot, which leads me to think that the recreation/recycling of tableCells might affect this? Can you put a NSlog inside the cellForRowAtIndexPath: function and see if this bug is triggered when this is called? – Jack Feb 12 '14 at 16:22
  • @Ilario thanks but I tried already. Besides, the cells are getting the touch (when failing and when working), and they are inside the table, so the table position does not seem to be the problem. – Angel G. Olloqui Feb 13 '14 at 10:26
  • @JackWu That was actually a very good tip, but it did not work either. I have removed cell reuse to test it and still happens in same way. :( – Angel G. Olloqui Feb 13 '14 at 10:31
  • May I ask why are you using a tableview INSIDE a scrollview? That's like having a scrollview on a scrollview... – Lord Zsolt Feb 13 '14 at 10:40
  • @AngelGarcíaOlloqui Hmm...intersting, can you post the code where you log "expanded"? Is it in the delegate call? – Jack Feb 13 '14 at 15:28
  • @JackWu Yes, it is in my delegate method. I use a generic datasource that handles the click, so that is why I have not posted it, but the datasource works everywhere all around the app except here, so there are little chances that the issue is there. – Angel G. Olloqui Feb 13 '14 at 16:21
  • @LordZsolt it is not so easy to explain due to the view hierarchy that I need to build, but basically I need some headers that get stucked on top when scrolling the outer scroll view, while the table will have their own header views working as expected and also stacking on top the section header that is visible and scrolling the rest. If I use 1 table only then I will not be able to reproduce the same behaviour – Angel G. Olloqui Feb 13 '14 at 16:47
  • do you have aditional touch gesture recognizers on other views? – Roma Feb 16 '14 at 11:53
  • did you figure out a solution for this? – Tom Abraham Apr 16 '14 at 19:38
  • @AngelGarcíaOlloqui Did you figured it out yet – Manoj May 15 '14 at 10:57
  • No. What I did is a hack, adding a tap gesture recognizer to the table and on click just get the indexpath of the point and call the delegate tableView:willSelectRowAtIndexPath: and tableView:didSelectRowAtIndexPath: (if exist) – Angel G. Olloqui May 15 '14 at 15:38

14 Answers14

25

leave the inner UITableView's scrollEnabled property set as YES. this lets the inner UITableView know to handle scroll-related touches on the UIScrollView correctly.

Meet Doshi
  • 4,241
  • 10
  • 40
  • 81
Tom Abraham
  • 390
  • 4
  • 7
  • Thanks this saved me some time. I was moving a tableView co-ords and intentionally setting the scrollEnabled to NO and finding that the cell did select was only noticed on the second attempt. Re-enabling scrollEnabled resolved it. Thanks – StuartM Jun 17 '14 at 11:00
  • 3
    This is the answer. Yes it's true embedding a tableView inside a scrollView is not supported, but if you've never been one to follow the rules (like me), this is how to get around the issue mentioned! I'll also add that any embedded tableViews should set `bounces` to `NO` to avoid potentially odd scrolling behavior. – devios1 Sep 30 '14 at 19:01
  • I have a tableview inside another "tableview cell",and i had the issue that the inner table cell did select was only recognised on the second attempt.Changing the inner UITableView's `scrollEnabled` property to `YES` and `bounces` to `NO` solved the issue. – abhimuralidharan Nov 02 '15 at 11:32
24

From Apple Documentation, you shouldn't embed a UITableViewinside a UIScrollView.

Important: You should not embed UIWebView or UITableView objects in UIScrollView objects. If you do so, unexpected behavior can result because touch events for the two objects can be mixed up and wrongly handled.

Your problem is really related to what your UIScrollView does.

But if it's just to hide the tableview when needed (that was my case), you can just move the UITableView in its superview.

I wrote a small example here : https://github.com/rvirin/SoundCloud/

Rémy Virin
  • 3,379
  • 23
  • 42
  • 1
    It's weird though that the warning you cite is only in the documentation of UIWebView and not UITableView. + according to this: https://developer.apple.com/library/ios/documentation/WindowsViews/Conceptual/UIScrollView_pg/NestedScrollViews/NestedScrollViews.html it's possible at least to nest scroll views. Submitted feedback to the documentation... it's confusing. – User Mar 08 '15 at 16:05
16

I ran into this same problem and figured out a solution!!

You need to set the delaysTouchesBegan to true on your scrollview so that the scrollview sends its failed scrolled-gesture (i.e. the tap) to its children.

var delaysTouchesBegan: Bool - A Boolean value determining whether the receiver delays sending touches in a begin phase to its view.

When the value of the property is YES, the window suspends delivery of touch objects in the UITouchPhaseBegan phase to the view. If the gesture recognizer subsequently recognizes its gesture, these touch objects are discarded. If the gesture recognizer, however, does not recognize its gesture, the window delivers these objects to the view in a touchesBegan:withEvent: message (and possibly a follow-up touchesMoved:withEvent: message to inform it of the touches’ current locations).

https://developer.apple.com/library/ios/documentation/UIKit/Reference/UIGestureRecognizer_Class/index.html#//apple_ref/occ/instp/UIGestureRecognizer/delaysTouchesBegan

But there's a catch...it doesn't work if you do it directly on the scrollview!

// Does NOT work
self.myScrollview.delaysTouchesBegan = true

Apparently this is an iOS bug where setting this property doesn't work (thank you apple). However there's a simple workaround: set the property directly on the scrollview's pan gesture. Sure enough, this worked for me perfectly:

// This works!!
self.myScrollview.panGestureRecognizer.delaysTouchesBegan = true
Oren
  • 5,055
  • 3
  • 34
  • 52
  • Thanks! This worked for me. It's probably not a fix for the whole issue but it let me fix the immediate problem without having to refactor everything to remove the tableview from the scrollview. – eyuelt Aug 11 '15 at 21:20
3

It seems that your UiTableView doesn't recognize your tap. Did you try to use that :

- (BOOL)gestureRecognizer:(UIPanGestureRecognizer *)gestureRecognizer
shouldRecognizeSimultaneouslyWithGestureRecognizer:(UISwipeGestureRecognizer             *)otherGestureRecognizer
{
    if ([otherGestureRecognizer.view isKindOfClass:[UITableView class]]) {
        return YES;
    }
    return NO;
}

Note from apple:

called when the recognition of one of gestureRecognizer or otherGestureRecognizer would be blocked by the other. return YES to allow both to recognize simultaneously. the default implementation returns NO (by default no two gestures can be recognized simultaneously)

note: returning YES is guaranteed to allow simultaneous recognition. returning NO is not guaranteed to prevent simultaneous recognition, as the other gesture's delegate may return YES

Hope that will help.

Nicolas Bonnet
  • 1,275
  • 11
  • 15
3

Gesture recognizers won't work correctly for two embedded scroll views or subclasses.

Try a workaround:

  1. Use transparent, custom, and overlaying everything in cell UIButton with proper tag, or subclass UIButton and add a index path property and overwrite each time in reused cell.

  2. Add this button as a property to your custom cell.

  3. Add target for desired UIControlEvent (one or more) that points to your UITableViewDelegate protocol adopting class.

  4. Disable selecting in IB, and manually manage the selection from code.

This solution requires attention for cases of single/multi selection.

Wladek Surala
  • 2,590
  • 1
  • 26
  • 31
  • There is no gesture recognizer in my view. However, a similar thing to workaround is what I am doing now, but it requires some dirty hacks to mimic the table view tap behaviour. I would like to remove it :( – Angel G. Olloqui Feb 21 '14 at 21:20
  • By gesture recognizers i meant that some classes use similar behavior, like `UITableViewCell` gets selected by a tap or long press gesture. The workaround proposed by me worked for two embedded `UITableView` classes (I wanted to have horizontal table-like rows that can be navigated by a swipe) resulting in UI behavior just as expected (perfect management of cell selection and swiping of both table views). I can share the code if you like. – Wladek Surala Dec 03 '14 at 11:57
  • Its a bit hacky for sure. But if you really "don't have a choice" on nesting e.g. a tableView in a scrollview, this solution is the best and easiest among other equally "hacky" solutions. I ended up giving the tableViewCell a buttonPushed-closure in cellForRowAtIndexPath and my closure just calls tableView.selectRow... and then didSelectCell. Works very well. – pIkEL Mar 26 '15 at 11:26
2

I've encountered a UITableView with scrollEnabled being NO within a UIScrollView in some legacy code. I have not been able to change the existing hierarchy easily nor enable scrolling, but come up with the the following workaround for the first tap problem:

@interface YourOwnTableView : UITableView
@end

@implementation YourOwnTableView

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

    [super touchesCancelled:touches withEvent:event];

    // Note that this is a hack and it can stop working at some point.
    // Looks like a table view with scrollEnabled being NO does not handle cancellation cleanly,
    // so let's repeat begin/end touch sequence here hoping it'll reset its own internal state properly
    // but won't trigger cell selection (the touch passed is in its cancelled phase, perhaps there is a part
    // of code inside which actually checks it)
    [super touchesBegan:touches withEvent:event];
    [super touchesEnded:touches withEvent:event];
}

@end

Again, this is just a workaround working in my specific case. Having a table view within a scroll view is still a wrong thing.

aleh
  • 455
  • 3
  • 9
1

I would recommend to look for options like not letting your cell to be in highlighted state when you are actually scrolling the outer scroll view which is very easy to handle and is the recommended way. You can do this just by taking a boolean and toggling it in the below method

- (void)scrollViewDidScroll:(UIScrollView *)scrollView 
Rajesh
  • 850
  • 9
  • 18
  • I do not understand this answer. My problem is that the cell receives the touch event but the table does not call `didSelectRowAtIndexPath`, it has nothing to do with the highlighted state. – Angel G. Olloqui Feb 13 '14 at 10:35
  • @AngelGarcíaOlloqui Think from Apple's perspective. How would you distinguish a accidental touch of table view cell while scrolling outer most scroll view. There might be a minimum threshold duration for the table view's didSelectRowAtIndexPath method to be called. I am sure that gets called perfectly once your outer scrollview is not scrolling or already decelerating its scrolling motion. End user can easily understand that scenario, so you need not worry of this issue as a problem. – Rajesh Feb 13 '14 at 11:18
  • That would make sense. However, in my case I can reproduce this issue after the table finishes scrolling, even many seconds after that happens. – Angel G. Olloqui Feb 13 '14 at 16:17
1

The scrollview is trying to figure out whether the user's intention is to scroll or not, so it's delaying the initial touch on purpose. You can turn this off by setting delaysContentTouches to NO.

Stepan Hruda
  • 596
  • 5
  • 12
  • Thanks Stepan, that was actually what I thought at first, but I tried setting that property to NO already, and same bug seems to happen. – Angel G. Olloqui Feb 21 '14 at 21:18
1

I have the same problem with nested UITableView and have found a work-around for this:

innerTableView.scrollEnabled = YES;
innerTableView.alwaysBounceVertical = NO;

You'll need to set the height of the inner table view to match with the total height of its cells so that it'll not scroll when user scrolling the outer view.

Hope this helps.

  • But the problem with this approach is that you will load all cells at once, creating a performance issue (my table have hundreds of cells). I need the scrolling inside to get all benefits from cell reuse and lazy loading. – Angel G. Olloqui Aug 05 '14 at 16:17
  • in addition if you auto-load more cells when the user scrolls to the end then this will keep loading more cells. – Oren Jun 25 '15 at 02:27
1

My mistake was implementing the delegate method:

- (void)tableView:(UITableView *)tableView didDeselectRowAtIndexPath:(NSIndexPath *)indexPath

instead of the one I meant to implement:

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath

Hence only being called on the second cell being tapped, because that was when the first cell would be de selected. Stupid mistake made with the help of autocomplete. Just a thought for those of you who may wander here not realizing you've made the same mistake too.

LunaCodeGirl
  • 5,432
  • 6
  • 30
  • 36
1
  1. Drop a UIButton over your UITableViewCell and create the outlet as "btnRowSelect".
  2. In your view controller put this code in cellForRowAtIndexPath

    cell.btnRowSelect.tag = indexPath.row
    cell.btnRowSelect.addTarget(self, action: Selector("rowSelect:"), forControlEvents: .TouchUpInside)
    
  3. Add this function to your viewController as well-

    func rowSelect (sender:UIButton) {
    // "sendet.tag" give you the selected row 
    // do whatever you want to do in didSelectRowAtIndexPath
    }
    

This function "rowSelect" will work as didSelectRowAtIndexPath where you get the row"indexPath.row" as "sender.tag"

shubham
  • 611
  • 7
  • 16
0

As other answers say you shouldn't put a tableview in a scrollview. A UITableView inherits from UIScrollView anyway so I guess that's where things get confusing. What I always do in this situation is:

1) Subclass UITableViewController and include a property UIView *headView.

2) In the parent UIViewController create all the top stuff in a container UIView

3) Initialise your custom UITableView and add the tableView's view to the view controller full size

[self.view addSubview: self.myTableView.view];

4) Set the headView to be your UIView gubbins

self.tableView.headView = myHeadViewGubbins.

5) In the tableViewController method

-(UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger *)section;

Do:

if ( section == 0 ) {
    return self.headView;
}

Now you have a table view with a bunch of other shizzle at the top.

Enjoy!

Martin
  • 1,135
  • 1
  • 8
  • 19
  • Thanks for your suggestion Martin but this can also be used with content insets and some other ways. However, this is not what I need for my UI as I explained above. Thanks anyway. – Angel G. Olloqui Feb 21 '14 at 21:17
0

That it, if touch table view it will work properly. also with scroll view in same view controller also.

tableview.scrollEnabled = true;
Ravi Kumar
  • 1,356
  • 14
  • 22
0

I have the same issue, Then refer to "Nesting Scroll Views" as lxx said.

https://developer.apple.com/library/ios/documentation/WindowsViews/Conceptual/UIScrollView_pg/NestedScrollViews/NestedScrollViews.html

An example of cross directional scrolling can be found in the Stocks application. The top view is a table view, but the bottom view is a horizontal scroll view configured using paging mode. While two of its three subviews are custom views, the third view (that contains the news articles) is a UITableView (a subclass of UIScrollView) that is a subview of the horizontal scroll view. After you scroll horizontally to the news view, you can then scroll its contents vertically.

It is work

Davin Kao
  • 1
  • 1