Problems accessing touch events in a UITableView (UIScrollView)

Discussion in 'iOS Programming' started by johnnyjibbs, Jan 25, 2009.

  1. johnnyjibbs macrumors 68030

    johnnyjibbs

    Joined:
    Sep 18, 2003
    Location:
    London, UK
    #1
    Hi all,

    I'm tearing my hair out trying to get my own custom horizontal swipe gestures to work on a UITableViewCell.

    Normally, the swipe gesture invokes the delete button method but I have switched that off because I want a swipe to mean a different method.

    I have set up the swipe gesture and it can work, but the UIScrollView (which UITableView inherits) is intercepting the touch events. I have subclassed both the table view itself and the table view cell but the cell only receives touch events (using touchesBegan and touchesMoved methods) when the table view has its scrollEnabled property set to NO. However, this obviously means that then the table view does not scroll.

    I know that UIScrollView (which UITableView inherits) does some nasty things to the responder chain, but there must be a way of tracking gestures and still allowing scrolling to be enabled. It must be possible because the application Tweetie uses its own fully functioning custom swipe gestures on table view cells.

    So the problem I have is that I've got the choice of either my swipe gestures working on cells but no table view scrolling OR table view scrolling but no swipe gestures!!! I need both, which is obviously possible, but I'm not sure what to do to get it working.

    The same behaviour happens whether I implement the swipe code in the custom table view subclass or in the custom table view cell subclass. I think the delaysContentScrolling property has part of what I'm looking for but it appears not to do what it says on the tin.

    UIScrollView itself has two methods that sound promising (the touchesBegan:withEvent:inContentView and touchesCancelled: ) but again I can't find a combination that gives me what I want.

    If anyone knows how to implement custom swiping gestures in a UITableViewCell, I'd much appreciate any help as to where I may be going wrong.

    Many thanks,
    John
     
  2. johnnyjibbs thread starter macrumors 68030

    johnnyjibbs

    Joined:
    Sep 18, 2003
    Location:
    London, UK
    #2
    Judging by the number of reads but no replies, I assume there is interest in this subject so I thought I would share with you how I have managed to (finally) get it to work.

    The key thing is that UIScrollView (from which UITableView inherits from) messes around with the responder chain so as to always return itself in the UIView method HitTest: WithEvent: It is this method that proved to be the key to getting the scroll view to play ball. (Note: it is important NOT to implement the touchesBegan, etc methods in this subclass or else you will override the default scrolling behaviour.)

    Therefore, it is vital to subclass the UITableView and override the HitTest: WithEvent method so that it returns the current UITableViewCell (which can be obtained by locating the cellAtPoint: UITableView method) unless you determine that there is some vertical finger movement or it is scrolling (i.e. where tableView.decelerating == NO), in which case return self.

    The table cell itself also needs to be subclassed and all the UIResponder methods touchesBegan: , touchesMoved and touchesEnded methods must be implemented. There you can pass a method to determine whether there is any vertical movement and, if so, set the next responder to be the table view. It is also here where you implement the code to work the swiping action (Apple's example code in the Events handling guide works well here).

    Then, it's just a case of cleaning up a little by resetting the various parameters in the touchesEnded method. I have a tick button on the left of my cell, so I made sure that the hitTest method in UITableView subclass always returned self if the touch.x position was < 50, but you get the gist.

    It is important to make sure that the delaysContentTouches property of the UITableView is set to YES or else you won't get any scrolling. It is also a good idea to turn off the swipe editing gesture because it will interfere with the custom swiping action.

    This is how I managed to get a table view cell to respond to touch swipe gestures but still allow the table view to scroll as intended.

    I spent many nights tearing my hair out trying to get this to work, so will be happy to provide some examples if anyone else has any particular problems trying to get this sort of thing to work.
     
  3. cxi macrumors newbie

    Joined:
    Jan 23, 2009
    #3
    I'm having the same problem right now and some sample code would be great

    thanks
     
  4. johnnyjibbs thread starter macrumors 68030

    johnnyjibbs

    Joined:
    Sep 18, 2003
    Location:
    London, UK
    #4
    hi cxi - sure - I'll post some code when I get a chance, hopefully tonight. Just arrived back from holiday!
     
  5. xcode13 macrumors newbie

    Joined:
    Feb 24, 2009
    #5
    Hey was just wondering if you'd had the chance to post some sample code for that yet?
    I'm also trying to do the same thing as you

    thanks
     
  6. PmattF macrumors member

    Joined:
    Dec 28, 2006
    #6
    Opposite Problem

    I am having more or less the opposite problem - I can't get my table to accept the drag events I want it to.

    In the built-in Photos app, in the thumbnail view for an album, if you press down on a thumbnail and hold for a moment, that thumbnail becomes selected. If you then drag, the selection gets cancelled, and the whole thumbnail grid scrolls.

    I am trying to do something similar – I have a table, with custom cells that contain four square image buttons. I have a method registered with the Touch Up Inside event on each button to select that thumbnail. It looks and works just like the Photos app, except for one thing. When I press and hold a thumbnail for a moment it becomes selected, but when I then drag, nothing happens. If I drag far enough out of the button it does become de-selected, but the enclosing table never starts scrolling.

    I could handle the Touch Drag Inside event, but what do I do with it? Can I force my button to resign its selected state, and pass the drag on to the table? If so, how?
     
  7. johnnyjibbs thread starter macrumors 68030

    johnnyjibbs

    Joined:
    Sep 18, 2003
    Location:
    London, UK
    #7
    Hi all - sorry for the delay in posting some sample code... Please read this walkthrough in conjunction with posts 1 & 2 in this thread. Here we go...

    Firstly, in your table view, you need to create an instance of a custom subclass of a UITableViewCell rather than the default implementation under the cellForRowAtIndexPath method:

    Code:
    CustomCell *cell = [[[CustomCell alloc] initWithFrame:rect reuseIdentifier:identifier] autorelease];
    Your CustomCell subclass will contain the touchesBegan, touchesMoved and touchesEnded methods to interact with the swipes. In the CustomCell.m, you would have some code a bit like this:

    Code:
    #pragma mark Touch gestures for custom cell view
    
    #define HORIZ_SWIPE_DRAG_MIN  12
    #define VERT_SWIPE_DRAG_MAX    4
    
    - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
        UITouch *touch = [touches anyObject];
        startTouchPosition = [touch locationInView:self];
    	self.swiping = NO;
    	self.hasSwiped = NO;
    	self.hasSwipedLeft = NO;
    	self.hasSwipedRight = NO;
    	self.fingerIsMovingLeftOrRight = NO;
    	self.fingerMovingVertically = NO;
    	[self.nextResponder touchesBegan:touches withEvent:event];
    }
    
    
    - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
    	if ([self isTouchGoingLeftOrRight:[touches anyObject]]) {
    		[self lookForSwipeGestureInTouches:(NSSet *)touches withEvent:(UIEvent *)event];
    		[super touchesMoved:touches withEvent:event];
    	} else {
    		[self.nextResponder touchesMoved:touches withEvent:event];
    	}
    }
    
    
    // Determine what kind of gesture the finger event is generating
    - (BOOL)isTouchGoingLeftOrRight:(UITouch *)touch {
        CGPoint currentTouchPosition = [touch locationInView:self];
    	if (fabsf(startTouchPosition.x - currentTouchPosition.x) >= 1.0) {
    		self.fingerIsMovingLeftOrRight = YES;
    		return YES;
        } else {
    		self.fingerIsMovingLeftOrRight = NO;
    		return NO;
    	}
    	if (fabsf(startTouchPosition.y - currentTouchPosition.y) >= 2.0) {
    		self.fingerMovingVertically = YES;
    	} else {
    		self.fingerMovingVertically = NO;
    	}
    }
    
    
    - (BOOL)fingerIsMoving {
    	return self.fingerIsMovingLeftOrRight;
    }
    
    - (BOOL)fingerIsMovingVertically {
    	return self.fingerMovingVertically;
    }
    
    // Check for swipe gestures
    - (void)lookForSwipeGestureInTouches:(NSSet *)touches withEvent:(UIEvent *)event {
        UITouch *touch = [touches anyObject];
        CGPoint currentTouchPosition = [touch locationInView:self];
    	
    	[self setSelected:NO];
    	self.swiping = YES;
    
    	ShoppingAppDelegate *appDelegate = (ShoppingAppDelegate *)[[UIApplication sharedApplication] delegate];
    	
    	if (hasSwiped == NO) {
    	
    	// If the swipe tracks correctly.
        if (fabsf(startTouchPosition.x - currentTouchPosition.x) >= HORIZ_SWIPE_DRAG_MIN &&
            fabsf(startTouchPosition.y - currentTouchPosition.y) <= VERT_SWIPE_DRAG_MAX)
        {
            // It appears to be a swipe.
            if (startTouchPosition.x < currentTouchPosition.x) {
    			hasSwiped = YES;
    			hasSwipedRight = YES;
    			swiping = NO;
    			appDelegate.updateCurrentItemQuantityCell = self; // Tell the app delegate which cell we are so we know which one to update
    			[[NSNotificationCenter defaultCenter] postNotificationName:@"gestureDidSwipeRight" object:self];
    		} else {
    			hasSwiped = YES;
    			hasSwipedLeft = YES;
    			swiping = NO;
    			appDelegate.updateCurrentItemQuantityCell = self;  // Tell the app delegate which cell we are so we know which one to update
    			[[NSNotificationCenter defaultCenter] postNotificationName:@"gestureDidSwipeLeft" object:self];
    		}
        } else {
            // Process a non-swipe event.
        }
    		
    	}
    }
    
    
     - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
    	 self.swiping = NO;
    	 self.hasSwiped = NO;
    	 self.hasSwipedLeft = NO;
    	 self.hasSwipedRight = NO;
    	 self.fingerMovingVertically = NO;
    	 [self.nextResponder touchesEnded:touches withEvent:event];
    }
    
    Note that some of the above code may be slightly redundant as I may have a couple of instance variables that are no longer neccessary. Basically, what happens is this:

    When the user touches the cell, the touchesBegan method is called and it sets all the values to NO (defaults), before sending the event to the next responder.

    In the touchesMoved method, it then tracks the current touch location and compares it to the start location (as defined in touchesBegan) to determine if the hand is swiping just a little bit left/right (horizontal motion) or not. This will in turn determine whether to scroll the table view or whether to keep looking for a swipe gesture. At this stage, we are not handling the actual swipes, just detecting whether there is any horizontal motion or not (within a small tolerance).

    The lookForSwipeGestureInTouches method handles the actual swipe gesture code - the values are taken from Apple's Event Handling Documentation example code. Here, if a swipe has been detected, a message is sent to the default NSNotificationCenter and the appropriate method in my main view controller is called so I can carry out the code that gets run once the swipe occurs.

    Finally, the touchesEnded method sets everything back to normal (i.e. cancels it).

    More in the following post...
     
  8. johnnyjibbs thread starter macrumors 68030

    johnnyjibbs

    Joined:
    Sep 18, 2003
    Location:
    London, UK
    #8
    Continued... Now that you have the swiping configured, you now need to subclass the UITableView to let it handle events properly. This is because UITableView inherits from UIScrollView, and UIScrollView plays havoc with the event queue by intercepting and cancelling all events.

    So, subclass the UITableView that you are using (e.g. CustomTableView) and implement the following hitTest method within it:

    Code:
    #pragma mark Touch gestures interception
    
    - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
    {
    	if (self.decelerating || self.editing == YES) {
    		// don't try anything when the tableview is moving..
    		return [super hitTest:point withEvent:event];
    	}
    	
    	// Do not catch as a swipe if touch is inside the area of the UIControl (check switch) or far right (potential index control)
    	if (point.x < 50 || point.x > 290) {
    		return [super hitTest:point withEvent:event];
    	}
    	
    	// Do not swipe if the global option is turned off
    	ShoppingAppDelegate *appDelegate = (ShoppingAppDelegate *)[[UIApplication sharedApplication] delegate];
    	if (appDelegate.globalSwipesOn == NO) {
    		return [super hitTest:point withEvent:event];
    	}
    	
    	// Find the cell
    	NSIndexPath *indexPathAtHitPoint = [self indexPathForRowAtPoint:point];
    	id cell = [self cellForRowAtIndexPath:indexPathAtHitPoint];
    	// forward to the cell unless we desire to have vertical scrolling
    	if (cell != nil && [cell fingerIsMovingVertically] == NO) {
    		return (UIView *)[cell contentView];
    	}
    	return [super hitTest:point withEvent:event];
    }
    
    
    
    - (BOOL)touchesShouldCancelInContentView:(UIView *)view {
    	return YES;
    }
    
    
    - (BOOL)touchesShouldBegin:(NSSet *)touches withEvent:(UIEvent *)event inContentView:(UIView *)view {
    	return YES;
    }
    
    All this does is return the cell view as the first responder UNLESS we want to specifically override the swipes for scrolling purposes (hence, we do not intercept touch gestures if the scroll view is currently scrolling/decelerating). You will notice that I have put some conditions in place so that the swipe gestures will not work on the very left of the cell (where I have an accessory view) nor the very right, nor if I have a global gestures setting turned to off.

    Once you have implemented this code, you should be able to get the desired effect. But of course please let me know if you get stuck along the way.
     
  9. johnnyjibbs thread starter macrumors 68030

    johnnyjibbs

    Joined:
    Sep 18, 2003
    Location:
    London, UK
    #9
    I'm not sure exactly but it sounds like your images (which sounds like you're using UIButtons) are holding onto the touch events and not passing them onto the table view afterwards.

    I suspect that the way to fix this would be to implement the HitTest:withEvent method for each of your buttons so that it returns the table view in certain scenarios (you would have to subclass UIButton to do this) but I'm not sure.

    Otherwise, it may be a case of playing around with some of the parameters e.g. delaysContentTouches in the table view.
     
  10. PmattF macrumors member

    Joined:
    Dec 28, 2006
    #10
    Yes, I am using four UIButtons with images loaded into them in a custom table cell. And yes, once a button has been selected, all future drag events are going to it. I have tried most everything to send them back out to the table once the drag leaves the button, and nothing works.

    delaysContentTouches is set – if it is not set then the problem becomes much worse, you can not scroll at all unless you put your finger down right between two images. Now the scroll works if you start moving quickly enough after the initial touch (i.e. the button does not have time to get selected). But if you pause, the button does get selected, and all the events seem to go to it, and never back out to the scrollview.

    Previously I had tried experimenting with setting userInteractionEnabled to NO on a dragOutside event. That does not send the current drag events out to the scrollview. It will send all future touches and drags out to the scroller if it is not turned back on.

    I then experimented with subclassing UIButton and tracking the touchesMoved events, and sending them to self.nextResponder and explicitly to the table, but that does not work.

    I had not experimented with hitTest, so based on your suggestion I just tried that. I added the hitTest method to my custom table cell, and set a flag when the button received a dragOutside event. Once the flag is set, I started returning self (i.e. the cell rather than the button) from hitTest instead of calling [super hitTest]. However, I have discovered that hitTest is called on the initial touch, and then on the initial drag, and then never called again during that interaction. In other words, once the system has decided that the current touch/drag/release cycle is going to the button, it never checks again.

    So I am really stumped. Obviously it works differently in the built-in Photos app. I have to wonder if they are doing something tricky with a private API to screw around with the event management, or whether they use a custom variant of UITableView.

    It is pretty annoying, because it definitely is easy to accidently select a button when you mean to scroll, and I really am I trying to get my UI to work as well as Apple's Photo app UI.
     
  11. penso macrumors newbie

    Joined:
    Mar 23, 2009
    #11
    PMatt.

    I have the same issue, I implemented the swipe properly.

    When the user swipes on the right, I remove the current view to display 3 buttons for specific features. I changed the hitTest:withEvent: in my own UITableView implementation, so the buttons works if the cell was swiped.

    However I then don't get the swipe at all, I'd like to catch it to show the original cell which was hidden on the original swipe, but only on a swipe and not on a click to the buttons.

    Any idea?
     
  12. xvrbx, Apr 22, 2009
    Last edited by a moderator: Aug 10, 2011

    xvrbx macrumors newbie

    Joined:
    Apr 22, 2009
    #12
    I have some comments/questions about the code snippets that you have posted:

    Code:
    - (BOOL)isTouchGoingLeftOrRight:(UITouch *)touch {
        CGPoint currentTouchPosition = [touch locationInView:self];
    	if (fabsf(startTouchPosition.x - currentTouchPosition.x) >= 1.0) {
    		self.fingerIsMovingLeftOrRight = YES;
    		return YES;
        } else {
    		self.fingerIsMovingLeftOrRight = NO;
    		return NO;
    	}
    	if (fabsf(startTouchPosition.y - currentTouchPosition.y) >= 2.0) {
    		self.fingerMovingVertically = NO;
    	} else {
    		self.fingerMovingVertically = NO;
    	}
    }
    Basically, the part that checks if (fabs(startTouch... is never reached, if you look at the previous if statement you are returning from the function always. And even if it was reached, self.fingerMovingVertically is always set to NO.

    Can you post a correction, please?

    Regards,
    Xavier
     
  13. johnnyjibbs, Apr 23, 2009
    Last edited by a moderator: Aug 10, 2011

    johnnyjibbs thread starter macrumors 68030

    johnnyjibbs

    Joined:
    Sep 18, 2003
    Location:
    London, UK
    #13
    Sorry that was an error, it was supposed to read YES in the upper statement.
     
  14. xvrbx, Apr 23, 2009
    Last edited by a moderator: Aug 10, 2011

    xvrbx macrumors newbie

    Joined:
    Apr 22, 2009
    #14
    Thanks for your quick reply!

    In my message I was not talking only about the fact both statements were returning NO, I'm also talking about something else:

    In the first IF statement, you are doing something like:

    Code:
    if (fabs... ) {
      ...
      return YES;
    } else {
      ...
      return NO;
    }
    
    So the code after that (the second IF statement) is never reached.

    Regards,
    Xavier
     
  15. johnnyjibbs, May 13, 2009
    Last edited by a moderator: Aug 10, 2011

    johnnyjibbs thread starter macrumors 68030

    johnnyjibbs

    Joined:
    Sep 18, 2003
    Location:
    London, UK
    #15
    Hi Xavier,

    Sorry you're right, having looked again at this - hmm.. I didn't notice this and I may revisit it but since it works as intended why fix it? :D
     
  16. AndrewKuzmin, Jun 15, 2009
    Last edited by a moderator: Aug 10, 2011

    AndrewKuzmin macrumors newbie

    Joined:
    Jun 15, 2009
    #16
    Hi, Johnny and everyone who is interested in custom swiping.

    Having read your posts found them really valuable (especially solution with overriding UIView method hitTest..., that was lacking information for me) - your solution implements custom swiping on cells. But I faced one thing I cannot handle : my application displays records in table view depending on period, so sometimes I have some records, sometimes I have none.
    First, I implemented UIResponder methods for touch handling in custom class subclassed from UITableView, but failed - swiping didn't work when I used the following code
    Code:
    - (UITableViewCellEditingStyle)tableView:(UITableView *)tableView editingStyleForRowAtIndexPath:(NSIndexPath *)indexPath
    {
      return UITableViewCellEditingStyleNone;
    }
    to remove default behaviour. (On that stage I understood something is wrong with UIScrollView behaviour)
    After that I tried your approach, worked fine in my case, but if there no records, thus no cells, thus no custom cell swipe, thus no next or previous period :( )
    The thing I want - that is to swipe on table view and switch to next or previous period.
    Maybe someone has some suggestions on this?

    P.S. In advance sorry for my imperfect English.

    Best regards,
    Andrew.
     
  17. johnnyjibbs thread starter macrumors 68030

    johnnyjibbs

    Joined:
    Sep 18, 2003
    Location:
    London, UK
    #17
    Yes, my code will only work on cells. For periods where there are no items in the table view and therefore no cells, maybe you could define a special case so that a cell would be created (with the words "No items" or something similar) so that you could swipe on? Otherwise, you'd have to have another view (which you could attach as the footer to the table) that uses most of the code that your custom cell uses.

    On another note, I'm going to have to revisit this code, since Apple's update has broken some of the functionality...
     
  18. AndrewKuzmin macrumors newbie

    Joined:
    Jun 15, 2009
    #18
    Thanks for your reply.

    Actually, I tried to implement something like swiping on header or footer, still not finished with that (the first option).
    Found one more interesting thing : if we have footer or header view that holds buttons and at the same time a cell view is "under" the footer or header, when pressing any of that buttons the first responder is table cell (invokes custom method for tapping), but not a button. Though when there is no table cell view "under", buttons are first responders (really not sure if subclassing of UIButton and implementing hitTest.. will help because according to code in hitTest... for table view cells will be first responders, probably there is some way to define whether it is footer or header view and to return them in hitTest...).
    Hope if I handle this and implement additional "No Items" cell I'll get a desired result (the second one) :rolleyes: .

    Best regards,
    Andrew.
     
  19. xoxoj macrumors newbie

    Joined:
    May 24, 2011
    #19
    hi johnny,


    I created a UIViewController view, and then on top in this view to add a UITableView, the UITableView completely covers the UIViewController, so UIViewController's touch fails, how to do?
     
  20. ShineLee macrumors newbie

    Joined:
    Aug 10, 2011
    #20
    hi johnny
    i had the same problems just some like you.but in my tableview,there's may only one cell that can not fill all the area of tableview.so when i swipe the blank area ,the firstresponder will be the tableview but event sometimes canceled by scrollview just like your problem,how can i fix this down? thanks
     

Share This Page