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?
#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.
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.
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/
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 .
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]
}
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
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..
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.
Related
I have a bunch of text fields in a registration form that are organized in two vertical stack views in a XIB file. I noticed that when running the app in the simulator I can press the Tab key and iOS will automatically move on to the next text field.
But this doesn't work sometimes, and I was wondering why. Sometimes the system focuses a text field from another stack view instead of the field below it.
I have already set up a chain of Next text fields (when pressing Next on the software keyboard) like this:
- (BOOL)textFieldShouldReturn:(UITextField *)textField {
NSUInteger index = [self.textFields indexOfObject:textField];
if (index != NSNotFound) {
if (index == self.textFields.count - 1) {
[textField resignFirstResponder];
[self createAccount];
} else {
UITextField *nextTextField = self.textFields[index + 1];
[nextTextField becomeFirstResponder];
}
return NO;
}
return YES;
}
textFields is an array of all text fields that this screen has from top to bottom.
I just want to be able to fill the form fast by typing a few characters and pressing Tab without additional mouse clicks. Is it possible?
It seems that -textFieldShouldReturn is not called during this "tab switch" so I can't control which text field becomes the next first responder. What is the trick for getting them to focus in the right order?
Check the index that returns by indexOfObject:. If you have some equal objects in the array, this method returns the index of the first of them.
If the index is wrong, use the tag property of UITextField like this:
UITextField * nextResponder = [self.textFields objectAtIndex:textField.tag + 1];
or if all of your text fields are in the same superview:
UITextField * nextResponder = (UITextField*)[textField.superview viewWithTag:textField.tag + 1];
Apple documentation:
Starting at index 0, each element of the array is passed as an argument to an isEqual: message sent to anObject until a match is found or the end of the array is reached. Objects are considered equal if isEqual: (declared in the NSObject protocol) returns YES.
https://developer.apple.com/documentation/foundation/nsarray/1417076-indexofobject?language=objc
This method defines what it means for instances to be equal. For example, a container object might define two containers as equal if their corresponding objects all respond YES to an isEqual: request. See the NSData, NSDictionary, NSArray, and NSString class specifications for examples of the use of this method.
If two objects are equal, they must have the same hash value. This last point is particularly important if you define isEqual: in a subclass and intend to put instances of that subclass into a collection. Make sure you also define hash in your subclass.
https://developer.apple.com/documentation/objectivec/1418956-nsobject/1418795-isequal?language=objc
I am building a forms based application and i have noticed that while in the simulator using the hardware mac keyboard i am able to tab between form fields in a table using the standard tab key.
Is there a way i can call this functionality from my TextView when the didReturn method is fired? I have seen numerous threads on here with various ways to achieve something similar but they all seem overly complex and bulky using view tags for big loops which is not ideal compared to perhaps just firing a TAB keyboard command?
All you need to do is set a nextButton on your view, or inputAccessoryView for all the textFields and on selector of that button write code to make nextTextField a first responder.
By this way you can implement that tab feature. As you know there's no tab button on your iPhone ;).
You can do it via UITextField delegate method:
- (BOOL)textFieldShouldReturn:(UITextField *)textField {
NSInteger nextTag = textField.tag+1;
UIResponder* nextResponder = [textField.superview viewWithTag:nextTag];
if (nextResponder) {
[nextResponder becomeFirstResponder];
}
else {
// Do what ever want to do for last textfield
[textField resignFirstResponder];
}
return NO;
}
Set you textfields tag, sequentially & set delegate. Pressing return key in for moving to next textfield.
Hope this helps.. :)
I have a relatively large iPhone application with many views.
I would like implement a next/previous option on my keyboard.
I have managed to implement it UI-wise, with some code examples i saw online, but all of them are assuming we need to add code to each view controller to implement the actual transition between the text fields.
My question is: is there a general way to know, given some text field, who is the next field in order? (i.e without refactoring each of my view controllers)
I ask this question because when i use the iPhone simulator and press the computer's Tab key - the switch between the fields happen, so i wonder if there is a built-in or generic way to implement it on iOS.
clarification:
is there a way of doing it without adding a specific code for each type of view controller? (adding a generic code is acceptable)
I want to write how i solved this problem, with the help of many good answers given to me here :)
First, i could not create fully generic code that creates tab regardless of the view it is in.
Instead i created this thing, which i think is the most generic solution with the firstResponder method not working:
i created custom toolbar with my next/previous/done buttons and appropriate actions delegate.
than i extended UIViewController by adding category "Tab". this category declares a fieldsArray and implements the delegate method.
Now what every specific view controller needs to do (beside importing the category) is to provide this fieldsArray according to its properties and calling the init method which adds the buttons toolbar to this fields
I hope you could benefit from this, and again thanks for all the good answers
you could have a method in a utility class that takes as arguments a textfield and a viewcontroller. then you could use the "tag"-attribute of the textfields to find the next textfield in that viewcotroller, assuming that you assigned the tags accordingly. numbers would be great, i think. a simple callback method in the vc could handle the focus-change. thats about as generic as i can see right now.
This is some generic code that I came up with:
// add a property for the fieldsArray
//add this in viewDidLoad
_fieldsArray = [[NSMutableArray alloc] init];
NSArray *viewsArray = [self.view subviews];
for (id view in viewsArray) {
if ([view isKindOfClass:NSClassFromString(#"UITextField")]) {
[_fieldsArray addObject:view];
}
}
//add this in your action that switches the fields
for (UITextField *field in _fieldsArray) {
if ([field isFirstResponder]) {
if ([fieldsArray lastObject] == field) {
[_fieldsArray[0] becomeFirstResponder];
}else {
NSUInteger nextIndex = [_fieldsArray indexOfObject:field] + 1;
[_fieldsArray[nextIndex] becomeFirstResponder];
}
break;
}
}
Before using it it should be improved.
1) find all subviews of self.view recursively
2) do some checks if the arrays are empty or nil or have just one object in them.
Good luck!
I am trying to implement a view with sliding side menus, such as with PKRevealController in iOS 6.1. A simple demo of this issue with source code on github is here, however you might not need to grab it if you already understand gestureRecognizer delegate implementation.
The problem I see is that two gestures that my users will want to use are going to be mutually confused for each other. The UITableView in the center (main screen) of the application should be able to use the swipe-right gesture to delete, but I still want a swipe that occurs across the top navigation area to result in exposing the side menus.I also intend to show other things than just the Table view, and at runtime I plan to swap out the main view with a different view, whenever a user selects a button on one of the side menus. This is kind of like a "hidden side tray UITabBarController" that I'm going for, but I want the side bars to be revealed only when the main "front view" controller is NOT a UITableView or its subviews.
Right now, using the demo sources that comes with PKRevealController, and adding deletion support to the main view's UITableView, no slide gesture to delete a row is possible. (You have to add one table view method to enable deletion support in the UITable view, which I did add.)
This was asked here, but the answer stated is incomplete, and as seen below, does not work for me and I have no idea why, because it appears that this delegate method is not invoked at any time where I return a YES, and yet it goes ahead anyways and begins a gesture.
Update The answer in the previous question is also wrong, as compared to the WIKI/FAQ answer I placed below.
I have only modified the class PKRevealController.m by adding this:
- (BOOL) gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
UIView *view1 = otherGestureRecognizer.view;
UIView *view2;
if (view1) {
view2 = view1.superview;
};
if ([gestureRecognizer.view isKindOfClass:[UITableView class]])
{
return NO;
}
// Co-operate by not stealing gestures from UITableView.
if ([view1 isKindOfClass:[UITableView class]]) {
return NO;
}else if ([view1 isKindOfClass:[UITableViewCell class]]) {
return NO;
// UITableViewCellContentView
}
else if (view2 && [view2 isKindOfClass:[UITableViewCell class]]) {
return NO;
// UITableViewCellContentView
}
else
{
return YES; // NEVER GETS HIT. BREAKPOINT HERE!
}
}
What confuses me is that at no point does the return YES code above get hit (I have a breakpoint on it) and yet, the Gesture controller is still stealing the gesture.
Note: I have made an evil hack, but I thought that I could prevent this cleanly. Here is my evil hack:
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer
{
if (gestureRecognizer == self.revealPanGestureRecognizer)
{
CGPoint translation = [self.revealPanGestureRecognizer translationInView:self.frontViewContainer];
BOOL begin = (fabs(translation.x) >= fabs(translation.y));
// BEGIN EVIL HACK
if (_topLimitY > 0) {
CGPoint location = [gestureRecognizer locationInView:gestureRecognizer.view];
if (location.y>_topLimitY) // _topLimitY = 55 for instance.
begin = NO;
}
// END EVIL HACK.
return begin;
}
else if (gestureRecognizer == self.revealResetTapGestureRecognizer)
{
return ([self isLeftViewVisible] || [self isRightViewVisible]);
}
return YES;
}
Right now in my evil hacked demo, I have set the topLimitY property (that I added to PKRevealController's properties) to 55, which allows me to swipe on the nav bar area of the front view, but not on the table view which takes up the rest of the demo.
Note that I plan to have multiple main views, and only want to defeat the gesture recognition on the whole main area if the view is a UITableView or some sub-view thereof. That is why I call my hack above a hack. Because I thought you could tell the gesture recognizer to go away and not bother you, and yet it doesn't work, it doesn't even invoke the shouldRecognize method, it just goes ahead and does the next thing in its list of things to do.
I should really read the WIKI first shouldn't I?
This is a FAQ, it says so right here:
When instantiating the controller pass this option in your options dictionary:
NSDictionary *options = #{
PKRevealControllerRecognizesPanningOnFrontViewKey : #NO
};
This will disable pan-based reveal for the entire front view. Now, you can use the revealPanGestureRecognizer and add it to any view you desire to be panned on that doesn't interfere with your table view, to enable gesture based reveal.
I'd advise (if working with a table based environment with swipe'able cells) you, to add the revealPanGestureRecognizer to your front view controller's navigation bar (which it most likely has):
[self.navigationController.navigationBar addGestureRecognizer:self.revealController.revealPanGestureRecognizer];
And voilĂ - panning doesn't interfere with your table view anymore.
more info at:
https://github.com/pkluz/PKRevealController/issues/76
Thank you Wiki. If only I had read it all first.
The above completely answers my question and was already there on the wiki. I'm answering my own question because it seems Google always comes to Stackoverflow first, and that might help other confused developers in the future.
Update If the above thing blows up when you try it, it's probably being done too early. Here's a slightly more robust version of the above fix:
// Additional gesture recognition linkups. The underscore variables here
// are implementation-section ivars in my app-delegate, that I have already
// checked are valid and initialized, and this is the last thing in my app delegate
// didFinishLaunch... method, before the return YES:
UIGestureRecognizer *rec = _revealController.revealPanGestureRecognizer;
if (rec) {
[_frontViewNavController.navigationBar addGestureRecognizer:rec];
}
Use This :
self.revealController.frontViewController.revealController.recognizesPanningOnFrontView = YES;
i am using a uitableview with one section and five cells, and two different uitextfields on each tableCell.
my problem is: when a user tabs on first textfield on first table row, keyboard comes up, then the user taps on the second textfield. the keyboard DISMISS and SHOW.
so how can i keep the keyboard up instead of DISMISS and SHOW when the user switching focus on textfields?
thanks.
first edit:
sorry for the late response on this, the code is a bit complicated to show in here. i do not resignFirstResponder/becomeFirstResponder in any of the textField delegate methods. could you please throw in any ideas on top of your head. thanks for all your help.
second edit:
sorry for my bad mistake, i totally misunderstood my problem. i will relink the new post in here in a minute.
third edit:
this is my new question
This should work for you... Just a guess work from my side as you had not posted any code:
-(BOOL)textFieldShouldReturn:(UITextField *)textField
{
if(textField == firstTextfield)
{
if([[firstTextfield stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]] length] == 0)
return NO;
[firstTextfield resignFirstResponder];
[secondTextField becomeFirstResponder];
return YES;
}
else if(textField == secondTextField)
{
//Anything u want here
return YES;
}
return NO;
}
Normally the keyboard doesn't dismiss on its own when you switch fields.
If you are using a textfield delegate and are responding to "editingDidEnd" by resigningFirstResponder, then you will see that behavior.
If that is the case, after you leave the field, it is calling one of the methods and resigning the keyboard, then when you touch the other field, it is calling the firstResponder.
So, look for some code where you are setting a textfield delegate to call one of the textfield selector methods. Find the method that is being called and see if it is resigingFirstResponder.
If it is, then you may need to remove it or wrap it in some appropriate logic.