[HOW-TO] Activate next UITextField in UITableView (iOS)

[HOW-TO] Activate next UITextField in UITableView (iOS)

February 8, 2013 09:52 3 comments
 

25 Kudos

Update: After that Matteo pointed out the fact that I had everything to be able to access to each UITableViewCell’s instances, I decided to update this blogpost accordingly.

One of the most convenient automations that a developer should build into his applications is the capability of moving to the next (text)fields in a form.

Unfortunately, this automation is not eased by Apple in its frameworks and, thus, require the developer to provide an extra-effort on his side to make this to happen.

My current scenario is the following:

Screenshot_08_02_13_10_11-3

As you can see, I have fields having textfields inside them, sections having a mix of them, sections having none of them. What I need is a way to get the textfields that I want to highlight. A trivial (non-working) solution one may come up with is to create a static variable and increment it each time a new cell is created in the table and assign this number to the tag property of each UITextField instance. This won’t work because of the memory management of UITableViews in iOS: each cell is reused so to lower the memory usage; what happens when you scroll down the table is to get the same cell that were used in the first row of the table (if you write good code). This won’t work because the time you go back to the top, your static variable (that keeps incrementing) will not be of the same value as it was before.

Therefore, my solution relies on the element that never changes (untrue but this is not a big issue) when working with tables: the NSIndexPath.
This solution simply computes the next valid indexPath.

This is the method that should make the magic to happen (make sure you’ve set your textField’s delegate):

- (BOOL)textFieldShouldReturn:(UITextField *)textField {
    [textField resignFirstResponder];
    YourCustomCell *currentCell = (YourCustomCell *) textField.superview.superview;
    NSIndexPath *currentIndexPath = [self.tableView indexPathForCell:currentCell];

    NSIndexPath *nextIndexPath = [self nextIndexPath:currentIndexPath];
    YourCustomCell *nextCell = [self.tableView cellForRowAtIndexPath:nextIndexPath];

    [self.tableView scrollToRowAtIndexPath:nextIndexPath atScrollPosition:UITableViewScrollPositionMiddle animated:YES];

    [nextCell.editField becomeFirstResponder]; //or whatever your property is called

    return YES;
}

What this method is doing is quite plain: it gets the currently selected cell, then the indexpath associated with it. Get the next indexpath and the cell associated to it. As Matteo suggests, if you have different UITableViewCell subclasses you can use introspection to act accordingly. Something like that should be working:

    ...
    UITableViewCell *nextCell = [self.tableView cellForRowAtIndexPath:nextIndexPath];

    if ([nextCell isKindOfClass:[YourCustomCell class]])
            [((YourCustomCell *)nextCell).editField becomeFirstResponder]; //or whatever your property is called
    else
         ...

    return YES;
}

As you probably already noticed, the core of the aforementioned method and its variant lies in the nextIndexPath: method. If you do not have particular needs, this method should simply return the indexpath of the next entry in the tableview that should be highlighted. In my scenario, this method skips all the rows that I do not want to be highlighted (because they also do not contain a textfield). For the lazy guys:

- (NSIndexPath *) nextIndexPath:(NSIndexPath *) indexPath {
    int numOfSections = [self numberOfSectionsInTableView:self.tableView];
    int nextSection = ((nextIndexPath.section + 1) % numOfSections);

    if (indexPath.row +1) == [self tableView:self.tableView numberOfRowsInSection:indexPath.section]) {
        return [NSIndexPath indexPathForRow:0 inSection:nextSection];
    } else {
        return [NSIndexPath indexPathForRow:(indexPath.row + 1) inSection:indexPath.section];
    }
}

This method works as follows: given an initial indexPath, it returns the indexpath having the next row (if the such row exists) or the indexpath of the next section at row 0. I used the modulo so to automagically go back to the top of the table when the end is reached.

I hope this helped! Thank you very much @Matteo for helping me finding a better solution.

UPDATE

As matt pointed out in the comments, if your cell is out of the view you are presenting, the tableview will return you a nil pointer. Then the question is: “how can we select the textview within a non-existing cell?”.

I came up with a solution that is really more a workaround than a clean solution.

- (BOOL)textFieldShouldReturn:(UITextField *)textField {
    // Scroll to the row
    [self.tableView scrollToRowAtIndexPath:nextIndexPath atScrollPosition:UITableViewScrollPositionMiddle animated:YES];

    // If you are scrolling to the top of the cell (in my case it means that if the user taps "enter" in the last row, therefore the next one will be the first one)
    // then setup a delay of 0.6 seconds
    double delayInSeconds = 0;
    if (nextIndexPath.section == 0 && nextIndexPath.row == 0) {
        delayInSeconds = 0.6;
    }

    // Retrieve the cell after the afore-defined delay
    dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
    dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
        UITableViewCell *nextCell = [self.tableView cellForRowAtIndexPath:nextIndexPath];
        YourCustomCell *advancedCell = (YourCustomCell *) nextCell;
        [advancedCell.editField becomeFirstResponder];
    });
}

This method scrolls to a specific NSIndexPath and retrieves the cell’s pointer after a given delay. It works fine but it’s rather a workaround. If somebody has a cleaner solution, please share it :)

  • http://twitter.com/Mckruz Matt kruczek

    What do you do when you have a very long UITableView and make a call to [self.tableView cellForRowAtIndexPath:nextIndexPath]; and you can’t get the next cell b/c it’s not in the viewable range?

    • http://twitter.com/elbryanlos Fabiano Francesconi

      I have updated the blogpost with a small example

      • http://twitter.com/Mckruz Matt kruczek

        Nice! Hey I’ll take it as a solution for now until something else comes along. Thanks Fabiano!