views:

930

answers:

5

Similar to this question I have a custom subclass of UITableViewCell that has a UITextField. Its working fine except the keyboard for doesn't go away when the user touches a different table view cell or something outside the table. I'm trying to figure out the best place to find out when something outside the cell is touched, then I could call resignFirstResponder on the text field.

If the UITableViewCell could receive touch events for touches outside of its view then it could just resignFirstResponder itself but I don't see any way to get those events in the cell.

EDIT: I tried this (below) in my UITableViewCell subclass but it doesn't work, I think because touchesBegan:withEvent: doesn't get called if the event was handled by a control. I think I need to catch the events before they get send down the responder chain somehow.

The solution I'm considering is to add a touchesBegan:withEvent: method to the view controller. There I could send a resignFirstResponder to all tableview cells that are visible except the one that the touch was in (let it get the touch event and handle it itself).

Maybe something like this pseudo code:

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    CGPoint touchPoint = // TBD - may need translate to cell's coordinates

    for (UITableViewCell* aCell in [theTableView visibleCells]) {
        if (![aCell pointInside:touchPoint withEvent:event]) {
             [aCell resignFirstResponder];
        }
    }
}

I'm not sure if this is the best way to go about this. There doesn't seem to be any way for the tableviewcell itself to receive event notifications for events outside its view.

EDIT2: I thought I had an answer (I even posted it as an answer) using hitTest:withEvent: but that didn't work out. It doesn't always get called. :-(

A: 

I think you're on the right track, but touchesBegan:withEvent: is a UIResponder method, so you'd actually have to override it in a UIView subclass rather than in your UIViewController subclass. Your options are:

  • If you're already subclassing UITableViewCell, override touchesBegan:withEvent: there.
  • If you're using a standard UITableViewCell, implement tableView:didSelectRowAtIndexPath in your UITableView's delegate.
Tom
I am subclassing UITableViewCell but I don't think overriding touchesBegan:withEvent: in there would work. I don't think the touches that are outside that view would be passed down to it.A UIViewController is a subclass of UIResponder, so I can override it there but I don't know if it will get all the touches or if they will be swallowed up in the tableview. I may just have to try and see what happens. Thanks!
progrmr
Aha! I'd never noticed that UIViewController is a UIResponder. But it's above its view in the responder chain, so you'd definitely miss some touches by overriding there.If you need to catch touches that fall outside the table cells, your best bet might be to overlay a transparent UIButton or UIView over the entire view. When the keyboard is not displayed, you can prevent that button/view from intercepting touches by setting `userInteractionEnabled=NO`.
Tom
+5  A: 

[Edited: removed previous attempt which didn't always work, this one does]

OK, I finally figured a solution that fully works. I subclassed UITableView and overrode the hitTest:withEvent: method. It gets invoked for all touches anywhere in the table view, the only other possible touches are in the navbar or keyboard and the tableview's hitTest doesn't need to know about those.

This keeps track of the active cell in the table view, and whenever you tap a different cell (or non-cell) it sends a resignFirstResponder to the cell going inactive, which gives it a chance to hide its keyboard (or its datepicker).

-(UIView*) hitTest:(CGPoint)point withEvent:(UIEvent*)event
{
    // check to see if the hit is in this table view
    if ([self pointInside:point withEvent:event]) {
        UITableViewCell* newCell = nil;

        // hit is in this table view, find out 
        // which cell it is in (if any)
        for (UITableViewCell* aCell in self.visibleCells) {
            if ([aCell pointInside:[self convertPoint:point toView:aCell] withEvent:nil]) {
                newCell = aCell;
                break;
            }
        }

        // if it touched a different cell, tell the previous cell to resign
        // this gives it a chance to hide the keyboard or date picker or whatever
        if (newCell != activeCell) {
            [activeCell resignFirstResponder];
            self.activeCell = newCell;   // may be nil
        }
    }

    // return the super's hitTest result
    return [super hitTest:point withEvent:event];   
}    

In my UITableViewCell subclasses that have a UITextField, I add the following code to get rid of the keyboard (or date picker, which slides up just like the keyboard):

-(BOOL)resignFirstResponder
{   
    [cTextField resignFirstResponder];  
    return [super resignFirstResponder];
}

Yay!

progrmr
Thank you! This worked for me.
juggleware
One more thing: for some reason it occasionally crashes. Change to this: if ([activeCell respondsToSelector:@selector(resignFirstResponder)]) { [activeCell resignFirstResponder]; }
juggleware
A: 

Hi

Thanks for providing this solution I'm hoping it will get me out of a sticky patch.

John Donovan
If my answer helps you out, it would be nice if you would vote it up.
progrmr
A: 

Dude thanks a lot for posting the code, I'm having the same issue.

Jigzat
If my answer helps you out, it would be nice if you would vote it up.
progrmr
A: 

That is a very good solution, the best I've found on the net. The only glitch I've discovered is that if you go from one cell with a textfield to another, the keyboard dismisses and reappears resulting in a jerky type animation.

dhvidding