3

I have a UITableViewController that has a UITextField in each cell. When the user hits return in a field, it automatically sets the next field to be first responder and scrolls the table to show that field, like this:

- (BOOL) textFieldShouldReturn:(UITextField *)textField 
{  
    UITextField *nextTextField=nil;
    NSInteger row;
    NSInteger section;

    if(textField == self.txtFieldA) 
    {
        nextTextField = self.txtFieldB;
        row=1; section=0;
    }
    else if(textField == self.txtFieldB) 
    {
        nextTextField = self.txtFieldC;
        row=2; section=0;
    }
    else 
    {
        [textField resignFirstResponder];
    }

    if(nextTextField) 
    {        
        NSIndexPath *indexPath = [NSIndexPath indexPathForRow:row inSection:section];
        [nextTextField becomeFirstResponder];

        [self.tableView scrollToRowAtIndexPath:indexPath atScrollPosition:UITableViewScrollPositionBottom animated:YES];      
    }
}

The issue comes when you select a text field (say txtFieldA), scroll the table down so that txtFieldB is not visible, then hit enter. It works as expected on iOS5 (txtFieldB becomes the new first responder). However, on iOS6 it doesn't. It scrolls to the correct spot in the table, but the txtFieldB does not become the first responder (and I'm not sure what is -- when I hit "Back" on my nav controller, the keyboard stays visible).

When I hit "Return", txtFieldB's didBeginEditing isn't fired. If I delay [nextTextField becomeFirstResponder] by a second or so (until the scrolling animation finishes which makes nextTextField visible) it works as expected, but it seems hacky and less smooth to do that so I would rather understand what's going wrong than implement something like that.

Ned
  • 6,280
  • 2
  • 30
  • 34

3 Answers3

2

In my solution to this problem, I subclassed UITableViewCell to make a text field cell. In that text field cell, I overrode the -setSelected:animated:.

- (void)setSelected:(BOOL)selected animated:(BOOL)animated
{
    [super setSelected:selected animated:animated];

    if (selected) {
        [self.textField becomeFirstResponder];
    }
}

Back in my view controller, when I want to advance to the next field, I just select the row it's in.

- (IBAction)advanceToNextTextField
{
    NSIndexPath *indexPath = [self nextIndexPathFromIndexPath:self.indexPathOfActiveField];

    [self.tableView selectRowAtIndexPath:indexPath
                                animated:YES scrollPosition:UITableViewScrollPositionNone];
    [self.tableView scrollToRowAtIndexPath:indexPath
                          atScrollPosition:UITableViewScrollPositionNone animated:YES];
}

This makes handling the the return key easy.

- (BOOL)textFieldShouldReturn:(UITextField *)textField
{
    switch (textField.returnKeyType) {
        case UIReturnKeyNext: [self advanceToNextTextField]; break;
        case UIReturnKeyDone: [self resignFirstResponder]; break;
        default:              break;
    }

    return YES;
}

Hope that helps.

Jeffery Thomas
  • 42,202
  • 8
  • 92
  • 117
  • That definitely looks like a better way to set it up, but am not sure that would solve the problem of hitting "Next" when the next cell is off the screen. Does this code work when you compile it on iOS6? – Ned Oct 05 '12 at 17:13
  • Yes, it works in iOS6. This code is pulled straight from my app which is designed to run in iOS5 and iOS6. – Jeffery Thomas Oct 05 '12 at 17:39
0

I also have an existing project that worked fine prior to iOS 6. My universal app uses a UITableView to create a form with "next" and "previous" buttons on a keyboard toolbar (similar to Safari). The table displays instances of a UITableViewCell subclass that contains a text field. The next/previous functionality also wraps - so "next" for the last field returns focus to the top of the form.

Unfortunately, Jeffrey Thomas' solution isn't quite working for me. If a cell is offscreen, the select-and-scroll method in advanceToNextField method works fine for the cell itself. In the setSelected method, the view hierarchy for the text field is correct: UITextField -> UITableViewCellContentView -> UITableViewCell subclass -> UITableView.

However, the becomeFirstResponder call is still failing to activate the field's editing mode, fire textFieldDidBeginEditing, etc. The keyboard is still visible but doesn't affect the now-visible cell.

For now, the only way that I've been able to circumvent this issue is to scroll first without animation:

[self.tableView scrollToRowAtIndexPath:indexPath atScrollPosition:UITableViewScrollPositionNone animated:NO];
[self.tableView selectRowAtIndexPath:indexPath animated:YES scrollPosition:UITableViewScrollPositionNone];

Like Ned's delay technique in the op, this ensures that the text field is visible when the app asks it to respond. Otherwise, it seems that becomeFirstResponder is firing during the animation & before the text field is (fully) visible. Apparently some changes in iOS 6 are more strict on first responder messages and visibility!

Sadly, this loses the nice fluid animation when moving from field to field. Assuming this isn't simply a bug in iOS 6.0, I plan on moving away from using UITableView as a form in the future.

Ogel
  • 11
  • 1
0

Figured out a solution to this problem, with the help of my coworker Charles (thanks!). I also tried Jeffrey Thomas's solution (and variations of it) with no luck: the text field does not become first responder if its table cell is offscreen, as described by Ogel as well. I looked for UITableViewDelegate methods that would alert me to when the tableview is done animating and delay calling the text field's setSelected: method until then, with no luck. Then he pointed me to

- (void)scrollViewDidEndScrollingAnimation:(UIScrollView *)scrollView

which is also fired by the table view (UITableViewDelegate conforms to UIScrollViewDelegate), when it is done scrolling in response to the table view method

- (void)scrollToRowAtIndexPath:(NSIndexPath *)indexPath atScrollPosition:(UITableViewScrollPosition)scrollPosition animated:(BOOL)animated

So the key is to calculate the index path of the row you intend to select in your next/previous method (like in Jeffrey Thomas's - (IBAction)advanceToNextTextField method), but instead of selecting it, just scroll to it, and set an ivar for it:

[_table scrollToRowAtIndexPath:nextIndexPath atScrollPosition:UITableViewScrollPositionMiddle animated:YES];
_selectIndexPathOnAnimationEnd = nextIndexPath;

Then in the scroll view delegate method, select the row at that index path after the table view has finished scrolling to it:

- (void)scrollViewDidEndScrollingAnimation:(UIScrollView *)scrollView {
    if(_selectIndexPathOnAnimationEnd != nil) {
        [_table selectRowAtIndexPath:_selectIndexPathOnAnimationEnd animated:NO scrollPosition:UITableViewScrollPositionMiddle];
        _selectIndexPathOnAnimationEnd = nil;
    }
}

At this point the text field can become first responder.

kurteous
  • 41
  • 3