48

Is there a way (either in IB or code) to set the tab order between text fields in a view?

Note that I'm not talking about the next form field after the return (or "Next") button is pressed -- many bluetooth keyboards have a tab key, which seems to cycle through the fields in completely different order. In my particular case, this order doesn't correspond to the fields' position in the view or even the order in which the fields were added. Modifying the xib file by hand to change the NSNextKeyView doesn't seem to make a difference either.

Does anyone know how to change this order?

lazycs
  • 1,586
  • 1
  • 13
  • 11
  • Nice question! I've been experimenting and there doesn't seem to be a way to change the order; it appears the tab order is always from top to bottom, but perhaps there *is* a hidden way to bias the order. – Kevin Yap Jun 23 '11 at 03:52
  • 1
    Strangely, the order that I'm experiencing ISN'T top to bottom, although I'd like it to be. That's why I asked in the first place :) – lazycs Jun 23 '11 at 14:35
  • Perhaps a Bluetooth keyboard + physical iPad function differently - I've only been testing it with the simulator and tabbing with my computer keyboard, assuming it would function the same (knowing the simulator, though, I probably shouldn't have assumed that). – Kevin Yap Jun 23 '11 at 16:47

8 Answers8

10

@sprocket's answer was only somewhat helpful. Just because something works out of the box doesn't mean you should stop thinking about a better way -- or even the right way -- of doing something. As he noticed the behavior is undocumented but fits our needs most of the time.

This wasn't enough for me though. Think of a RTL language and tabs would still tab left-to-right, not to mention the behavior is entirely different from simulator to device (device doesn't focus the first input upon tab). Most importantly though, Apple's undocumented implementation seems to only consider views currently installed in the view hierarchy.

Think of a form in form of (no pun intended) a table view. Each cell holds a single control, hence not all form elements may be visible at the same time. Apple would just cycle back up once you reached the bottommost (on screen!) control, instead of scrolling further down. This behavior is most definitely not what we desire.

So here's what I've come up with. Your form should be managed by a view controller, and view controllers are part of the responder chain. So you're perfectly free to implement the following methods:

#pragma mark - Key Commands

- (NSArray *)keyCommands
{
    static NSArray *commands;

    static dispatch_once_t once;
    dispatch_once(&once, ^{
        UIKeyCommand *const forward = [UIKeyCommand keyCommandWithInput:@"\t" modifierFlags:0 action:@selector(tabForward:)];
        UIKeyCommand *const backward = [UIKeyCommand keyCommandWithInput:@"\t" modifierFlags:UIKeyModifierShift action:@selector(tabBackward:)];

        commands = @[forward, backward];
    });

    return commands;
}

- (void)tabForward:(UIKeyCommand *)command
{
    NSArray *const controls = self.controls;
    UIResponder *firstResponder = nil;

    for (UIResponder *const responder in controls) {
        if (firstResponder != nil && responder.canBecomeFirstResponder) {
            [responder becomeFirstResponder]; return;
        }
        else if (responder.isFirstResponder) {
            firstResponder = responder;
        }
    }

    [controls.firstObject becomeFirstResponder];
}

- (void)tabBackward:(UIKeyCommand *)command
{
    NSArray *const controls = self.controls;
    UIResponder *firstResponder = nil;

    for (UIResponder *const responder in controls.reverseObjectEnumerator) {
        if (firstResponder != nil && responder.canBecomeFirstResponder) {
            [responder becomeFirstResponder]; return;
        }
        else if (responder.isFirstResponder) {
            firstResponder = responder;
        }
    }

    [controls.lastObject becomeFirstResponder];
}

Additional logic for scrolling offscreen responders visible beforehand may apply.

Another advantage of this approach is that you don't need to subclass all kinds of controls you may want to display (like UITextFields) but can instead manage the logic at controller level, where, let's be honest, is the right place to do so.

Christian Schnorr
  • 10,768
  • 8
  • 48
  • 83
  • I have tried to implement this method but i am not able to call self.controls; I have tried to call it in cell as well as in view controller. Can you please guide me. Thanks in advance. – Nikh1414 Jun 22 '15 at 10:00
  • @Nikh1414 You obviously have to implement such a method to return the control you want to tab around... – Christian Schnorr Jun 22 '15 at 10:01
  • If possible can you please provide me example and update your answer accordingly. – Nikh1414 Jun 22 '15 at 10:03
  • 1
    My example is clear enough. Just override the getter for `-controls` and return whatever controls you want to cycle through. – Christian Schnorr Jun 22 '15 at 10:50
  • 1
    *THIS* is the real answer of the 21st century. The others are hacks to kill your time and code. – Dimitris Aug 25 '16 at 21:20
4

I'm interested in solving the same problem, although so far the default order, which appears to be left to right, then top to bottom, is the one I want.

I tested the hypothesis that the cursor moves in depth-first order through the tree of subviews and superview, but that is not true. Changing the order of subviews without changing their location didn't change the order of fields traversed by tab presses.

One possibly useful feature is that the text field delegate's textFieldShouldBeginEditing method appears to be called for every text field in the application's window. If that returns NO, then the text field won't be chosen, so if you can define your desired order and make only the right one return YES, that might solve your problem.

sprocket
  • 1,217
  • 10
  • 10
  • I think this is probably the best way to do it since I can't find anyway to change the order from the default by modifying the xib file. What's odd is that my fields are not being selected in the left to right, top to bottom order by default. Thanks! – lazycs Aug 13 '11 at 01:51
  • Doesnt effect the behaviour though you change return value in the shoulbegin method. Whether it returns yes or no, control traverse through all the textfields in the view and obviously calls should begin method everytime but only field(i.e. nearest to current field) will become active. – vrk Nov 09 '12 at 09:01
  • I built a solution to the tab order problem based on this hypothesis. It detects and responds not only to tabs but also shift+tabs to travel backwards. – AlleyGator Nov 13 '12 at 19:27
3

The Tab key behaviour in ios will be as follows:- when u press tab on external keyboard- the control traverses across all the textfields in that screen by calling only shouldBeginEditing method where its return value is also determined by Apple which cant be override. After scanning all the fields it calculates nearest x positioned Textfield relative to view offset from the current Textfield and then nearest Y Positioned Field.

Also can't be done anything until control comes to textFieldDidBeginEditing method.

Reason for apple's restriction might be to let devs to follow the guidelines of UI where next responder of field should be it's closest positioned Field rather than any other field .

vrk
  • 125
  • 1
  • 9
3

This is how you set the tab order on iOS:

http://weaklyreferenced.wordpress.com/2012/11/13/responding-to-the-tab-and-shift-tab-keys-on-ios-5-ios-6-with-an-external-keyboard/

AlleyGator
  • 1,266
  • 9
  • 13
2

Register a UIKeyCommand to detect the tab key pressed. I did this in my current view controller.

    self.addKeyCommand(UIKeyCommand(input: "\t", modifierFlags: [], action: #selector(tabKeyPressed)))

Inside the key tabKeyPressed handler find your current active field then set your next responder. orderedTextFields is an array of UITextField in the tab order I want.

func tabKeyPressed(){
    let activeField = getActiveField()
    if(activeField == nil){
        return
    }
    let nextResponder = getNextTextField(activeField!)
    nextResponder?.becomeFirstResponder()
}

func getActiveField() -> UITextField? {
    for textField in orderedTextFields {
        if(textField.isFirstResponder()){
            return textField
        }
    }
    return nil
}

func getNextTextField(current: UITextField) -> UITextField? {
    let index = orderedTextField.indexOf(current)
    if(orderedTextField.count-1 <= index!){
        return nil
    }
    return orderedTextField[index! + 1]
}
0

I solved this by subclassing UITextField as NextableTextField. That subclass has a property of class UITextField with IBOutlet a hookup.

Build the interface in IB. Set the class of your text field to NextableTextField. Use the connections Inspector to drag a connection to the 'next' field you want to tab to.

In your text field delegate class, add this delegate method...

- (BOOL)textFieldShouldReturn:(UITextField *) textField
{
    BOOL didResign = [textField resignFirstResponder];
    if (!didResign) return NO;

    if ([textField isKindOfClass:[NextableTextField class]])
        dispatch_async(dispatch_get_current_queue(), ^{ [[(NextableTextField *)textField nextField] becomeFirstResponder]; });

    return YES;
}

BTW - I didn't come up with this; just remember seeing someone else's idea.

mputnamtennessee
  • 372
  • 4
  • 14
  • I don't think this will work by itself, because `textFieldShouldReturn:` is not called when the tab key is pressed. – ThomasW Apr 03 '18 at 02:25
0

You can do this by setting the tag for each textfield and handling this in the textfieldShouldReturn method.

See this blogpost about it: http://iphoneincubator.com/blog/windows-views/how-to-create-a-data-entry-screen

Milk78
  • 239
  • 3
  • 13
  • 3
    Thanks for the idea, but that only implements what I already have: the ability to select the next field when a user presses the return key. What I'm looking for is a way to select the next field on a *tab* press on a physical (e.g. blue tooth) keyboard. Setting the tag has nothing to do with the tab order as far as iOS is concerned; the blog post simply uses it as a convenient way to access the next text field in the delegate methods (which are only called when the *return* key is pressed). – lazycs Apr 20 '11 at 13:57
0

The only way I've found to uniquely detect a Tab keystroke from a physical keyboard, is implementing the UIKeyInput protocol's insertText: method on a custom object that canBecomeFirstResponder.

- (void)insertText:(NSString *)text {
    NSLog(@"text is equal to tab character: %i", [text isEqualToString:@"\t"]);
}

I didn't get this to work while subclassing UITextField, unfortunately, as UITextField won't allow the insertText: protocol method to get called.

Might help you on the way, though..

hsdev
  • 507
  • 3
  • 12
  • Tabbing between text fields is handled automatically by iOS... I don't really need to detect the tab keystroke manually; I just want to be able to set which element should become the first responder after another resigns as first responder automatically in response to the tab key. – lazycs Aug 05 '11 at 03:22
  • I know, what I'm trying to do is examine the possibility of detecting Tab keystrokes yourself and combine this with an array containing the order of the input fields, for example. In other words; replacing the iOS behaviour when it comes to tabbing. – hsdev Aug 05 '11 at 11:30