One of the more tedious things in iPhone development is to create functional data entry screens. The challenge is to fit the necessary text fields on the screen and deal with the keyboard that pops up and covers roughly half the screen. There is surprisingly little support for this in the SDK.
Data entry is also one of the more tedious activities for users of your iPhone app, so you should think long and hard about ways to eliminate data entry and make the UI for what you can’t eliminate as user friendly as possible.
This blog post may not offer UI nirvana, but it has some techniques that I’ve found useful on screens with multiple UITextField and UITextView controls. The methods described here are generic enough that they should fit most such screens.
The first step is to create your screen in Interface Builder (or in code if you prefer). The first UI element you need to add is a UIScrollView. It should cover the entire view and be configured to scroll vertically.
Then you place your UITextFields and UITextViews on the UIScrollView. Each data entry control needs to have its delegate set to the view controller so that the controller can act upon events from the controls. In Interface Builder you also configure captitalization, correction, keyboard type, etc for each field. With the exception of the scroll view, this is probably what most of your screens already look like.
Next we’re going to use the versatile “tag” attribute of UIView. The value is tag will be used similar to how tab order is used in other UI technologies. So set the tag to 1 for the data entry field at the top of your screen, 2 for the second, etc.
Now let’s dive into some code. Your controller needs to implement the protocols UITextFieldDelegate and UITextViewDelegate (assuming that you have both types of UI controls in your screen).
@interface MyViewController : UIViewController <UITextFieldDelegate, UITextViewDelegate>
Implement the following methods which are called when a text field or text view becomes the first responder, either by the user tapping inside the field or by your code explicitly requesting it to become the first responder.
#pragma mark UITextFieldDelegate - (void)textFieldDidBeginEditing:(UITextField *)textField { [self scrollViewToCenterOfScreen:textField]; } #pragma mark UITextViewDelegate - (void)textViewDidBeginEditing:(UITextView *)textView { [self scrollViewToCenterOfScreen:textView]; }
The scrollViewToCenterOfScreen simply takes the frame of the view and positions the scroll view so that the view is in the center of the visible area. The visible area may be significantly reduced by the space taken up by the keyboard. More on that later.
- (void)scrollViewToCenterOfScreen:(UIView *)theView { CGFloat viewCenterY = theView.center.y; CGRect applicationFrame = [[UIScreen mainScreen] applicationFrame]; CGFloat availableHeight = applicationFrame.size.height - keyboardBounds.size.height; // Remove area covered by keyboard CGFloat y = viewCenterY - availableHeight / 2.0; if (y < 0) { y = 0; } scrollView.contentSize = CGSizeMake(applicationFrame.size.width, applicationFrame.size.height + keyboardBounds.size.height); [scrollView setContentOffset:CGPointMake(0, y) animated:YES]; }
Notice that the calculations above used a variable, keyboardBounds.size.height, for the height of the keyboard. The lazy approach would be to just use a constant with the value 216. That would work fine in portrait mode with the current keyboards available in the current iPhone OS. But listen to Apple's warnings: Do not hardcode values like this in your code. Your app will break in the future.
One way to get the accurate size of the keyboard is to observe keyboard notifications.
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardNotification:) name:UIKeyboardWillShowNotification object:nil];
If your screen allows the keyboard to disappear, then you should also observe the UIKeyboardWillHideNotification.
The keyboardNotification method looks like this:
- (void)keyboardNotification:(NSNotification*)notification { NSDictionary *userInfo = [notification userInfo]; NSValue *keyboardBoundsValue = [userInfo objectForKey:UIKeyboardBoundsUserInfoKey]; [keyboardBoundsValue getValue:&keyboardBounds]; }
And the declaration of keyboardBounds:
CGRect keyboardBounds;
The above steps ensure that the current field where the user is entering data is made visible. Now for some extra credit let's use the return key on the keyboard to move from one field to the next. This is where the "tag" attribute will be used.
For UITextFields you can implement textFieldShouldReturn like this:
- (BOOL)textFieldShouldReturn:(UITextField *)textField { // Find the next entry field for (UIView *view in [self entryFields]) { if (view.tag == (textField.tag + 1)) { [view becomeFirstResponder]; break; } } return NO; }
What this method does is to look through an array of entry fields to find the one with the next higher tag value. The entryFields method looks like this:
/* Returns an array of all data entry fields in the view. Fields are ordered by tag, and only fields with tag > 0 are included. Returned fields are guaranteed to be a subclass of UIResponder. */ - (NSArray *)entryFields { if (!entryFields) { self.entryFields = [[NSMutableArray alloc] init]; NSInteger tag = 1; UIView *aView; while (aView = [self.view viewWithTag:tag]) { if (aView && [[aView class] isSubclassOfClass:[UIResponder class]]) { [entryFields addObject:aView]; } tag++; } } return entryFields; }
Taken together you now have a pretty good framework for creating usable data entry screens. Please let me know in the comments any improvements you make or any alternative techniques you've used.
May 3rd, 2009 at 09:11
minor suggestion; I’d return [entryFields autorelease] from
– (NSArray *)entryFields
or rename it to createEntryFields…
May 4th, 2009 at 07:10
@Rob: I neglected to show in the code snippets that entryFields is declared as a property.
@property (nonatomic, retain) NSArray *entryFields;
Since this method is called multiple times while the user is interacting with the screen, I want to allocate memory for the array once and keep it around. This might be a slight misuse of @property since no code outside of this class will ever access entryFields. But it cuts down on the amount of code I have to write for memory management. 😉
May 3rd, 2009 at 10:26
Good tips and agree that it is surprising how lacking the SDK is in this area.
May 3rd, 2009 at 11:58
Typo: “needs to have it’s delegate” –> “needs to have its delegate”
May 3rd, 2009 at 11:59
Typo: “the heigh of the keyboard” –> “the height of the keyboard”
May 3rd, 2009 at 12:02
The point about hard-coding a value for the keyboard height is interesting because Apple’s own code samples do exactly that. See, for example, the kOFFSET_FOR_KEYBOARD constant in their UICatalog sample. They clearly do not practice what they preach.
May 4th, 2009 at 04:47
Great tutorial, I have always wondered about the problems the keyboard could create by popping up in your face.
May 4th, 2009 at 05:40
Very nice. However, there’s a potential infinite loop in – (NSArray *)entryFields. If a view is found that does not satisfy the innermost ‘if’ statement, the code will loop forever.
May 4th, 2009 at 07:01
@Frank: Good catch. Thanks!
May 4th, 2009 at 09:26
I have a re-usable class that allows you to define a form in a table. This supports editing existing data or creating new data. Even though it’s canned, it is fairly customizable. Please read about it here: http://osmorphis.blogspot.com/2009/04/tableformeditor-v14-text-field-settings.html
May 5th, 2009 at 06:53
Thanks, this is an interesting and very usable approach.
I’m having a hell of a time adapting this to a grouped UITableView with controls inside the cells, the behavior is rather erratic: when an UITextField which is below the keyboard grabs the focus the table scrolls to an arbitrary position. In my case with 5 fields + section titles it jumps to precisely 98.0 points.
For the cells I’m using CellTextFields instances from the UICatalog samples.
Do you have any success using this technique with UITableViews?
May 5th, 2009 at 12:57
@Clawoo: Check out John’s TableFormEditor class. URL in his comment above.
May 6th, 2009 at 00:45
very good article, thanks
May 16th, 2009 at 05:11
I’m having trouble getting this code working in my app. There are no build errors but no matter what I do textFieldDidBeginEditing and scrollViewToCenterOfScreen won’t fire.
Any chance of posting a working sample for download so I compare it with mine?
Cheers
June 11th, 2009 at 16:29
Roland, this is a month later, but…
you may have to
[myTextField setDelegate:self];
June 25th, 2009 at 04:49
Hey there,
This worked almost perfect for me except for one thing. If the scrollview is at the top (i.e. on first load) then the scroll view snaps back to the top after it scrolls to the textfield. If you immediately touch the textfield again or scroll a bit down first then it works ok.
Any ideas why this would happen, its driving me nuts!
August 2nd, 2009 at 17:49
Karl, any luck?
I’m having the same thing happen only slightly weirder. I have two text fields, the top one works perfectly but the bottom one (immediately below the top one) behaves just as you explain, “snaps back” after it starts scrolling the desired direction.
If you found any fix, please post!
August 21st, 2009 at 09:38
I’ve inserted the code into my app for the NSArray but I get “error: ‘entryFields’ undeclared (first use in this function)” right after “if (!entryFields) { . I then also get an error of “warning: control reaches end of non-void function” after “return entryFields; } . What am I missing?
September 7th, 2009 at 05:53
Great article. This helped a bunch!
November 16th, 2009 at 18:21
Trying to get this logic to work! I am assuming that most of the above code should be implemented in the .m file of the controller file that is being created.
Everything compiles but when I try to enter a textField field I get this:
2009-11-16 20:23:09.504 TestMe[1287:20b] *** -[CreateNewTestViewController keyboardNotification]: unrecognized selector sent to instance 0x3f30bb0
2009-11-16 20:23:09.506 TestMe[1287:20b] *** Terminating app due to uncaught exception ‘NSInvalidArgumentException’, reason: ‘*** -[CreateNewTestViewController keyboardNotification]: unrecognized selector sent to instance 0x3f30bb0’
Any suggestions?
Thanks
December 12th, 2009 at 16:35
@Karl and @Jason,
I *might* have figured out the jump problem. had the exact same issue in an app I’m working on right now. Coincidentally, it’s my first iPhone app, so my understanding and explanation might be a bit limited.
Here’s the solution that works for me. I assume it should work for you guys, too. scroll up and look at line 11 of the scrollViewToCenterOfScreen: method. Instead of adding the keyboard height, it works perfectly well when I *subtract* the keyboard height:
scrollView.contentSize = CGSizeMake(applicationFrame.size.width, applicationFrame.size.height – keyboardBounds.size.height);
That’s the same as saying:
scrollView.contentSize = CGSizeMake(applicationFrame.size.width, availableHeight);
OR you could delete that line altogether and have the same result.
It makes sense to me because you’re supposed to be making that scrollView smaller, not adding to make it larger (the height PLUS the height of the KB). Because you can delete the line and still have the same results, it tells me that the size of the scrollView is unimportant, but you do have to know the point where they offset is located. The only side effect I see is that the scrollView doesn’t scroll when the keyboard is activated. Personally I like it that way, as it keeps focus on where the user is typing.
This method hasn’t been checked with textViews – only textFields. YMMV
December 12th, 2009 at 16:43
And now looking back at my reasoning – after my comment – I see what’s going on. The size of the content inside, not the size of the scrollView, is getting resized. So I understand how that should work, but now I’m back to square one in understanding why it doesn’t work as intended.
Any ideas, Nick?
March 31st, 2010 at 02:34
Sir,This is good example
but can u give me document with explanation of each and every ling broadly to understand ur concept
2>Is ur code is working on Portrait,Landscape,Portrait to Landscape and Landscape to portrait mode
March 31st, 2010 at 14:07
@Hardik: The code above is not dependent on portrait or landscape mode. If your screen layout works in both orientations, then this code should work too.
April 17th, 2010 at 07:58
Thanks for the example you don’t even know how many books and the SDK itself I browsed to even get a little of the knowledge you just dropped. I clipped this into evernote just in case I can’t access your site in the future. Thanks, again, and know that I’m using it.
April 27th, 2010 at 00:33
Here is the solution to the snap problem and the code to scroll view down after editing:
//We register for keyboard notifications in View init:
[self registerForKeyboardNotifications];
//Add this func to you View .m
– (void)registerForKeyboardNotifications{
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(keyboardWasShown:)
name:UIKeyboardDidShowNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(keyboardWasHidden:)
name:UIKeyboardDidHideNotification object:nil];
}
//We add a implementation for the return key
– (BOOL)textFieldShouldReturn:(UITextField *)aTextField {
[myScrollView setContentOffset:CGPointMake(0, 0) animated:YES];
return YES;
}
//We move the View resize funcs out of textFieldDidBeginEditing
//The bKeyboardWasShown is for smooth scrolling when the keyboard is already on screen
– (void)keyboardWasShown:(NSNotification*)aNotification{
if(!bKeyboardWasShown){
bKeyboardWasShown=YES;
CGRect applicationFrame = [[UIScreen mainScreen] applicationFrame];
myScrollView.contentSize = CGSizeMake(applicationFrame.size.width, applicationFrame.size.height + 216);
}
}
// Called when the UIKeyboardDidHideNotification is sent
– (void)keyboardWasHidden:(NSNotification*)aNotification{
bKeyboardWasShown=NO;
CGRect applicationFrame = [[UIScreen mainScreen] applicationFrame];
myScrollView.contentSize = CGSizeMake(applicationFrame.size.width, applicationFrame.size.height);
}
//And finally, add this to scrollViewToCenterOfScreen to check that the view won’t scroll too much down
if (y > 216) {
y = 216;
}
April 27th, 2010 at 00:34
EDIT: Add the necessary funcs and variables to your View .h
May 9th, 2010 at 08:55
Stop. Please never make a data entry screen out of fields in a scrollview. Always use a UITableView; also, scrolling is done automatically for you when you do it the way I suggest.
May 9th, 2010 at 20:18
@Jonathan: In some cases a table view makes more sense, i.e. when all data entry fields are uniform in size and type, in other cases I think using a scroll view as described above is a good solution. Would you like to expand on your reasons for “never” making a data entry screen using a scroll view?
May 10th, 2010 at 22:44
@Nick: It’s not standard UI, and it’s just plain ugly.
July 12th, 2010 at 12:09
Thanks for this post. Would be great to see it updated showing entryFields added as a property and synthesized. But once I figured that out it worked like a charm in my code. Greatly improved my data entry screen and I learned a think or two!
July 29th, 2011 at 17:02
[[aView class] isSubclassOfClass:[UIResponder class]]
should be written as
[aView isKindOfClass:[UIResponder class]]
November 10th, 2011 at 09:32
Thanks for the post. What do you think about splitting data entry tasks into successive views (with next/ back and progress feedback) so the user doesn’t have to scroll and worry about the keyboard covering up the more info. More development work for sure, but perhaps better for high-frequency data entry. I am trying to figure out a data entry design and am torn about the approach. Thanks!
December 6th, 2011 at 17:35
I have tried this code, and I cannot get the values from the textfields.
I am fairly certain I set the outlets correctly in IB. The scrolling, the textFieldShouldReturn to the next field, the textfield centering, … all that works but when I try this:
NSString *strCal = txtfieldCal.text;
NSLog(@”cal entered: %a”, strCal);
I get:
calories Entered: -0x1.fdbe804e6f74p+0
Any ideas?
December 7th, 2011 at 16:42
please disregard that last question. I realized it should have been %@ not %a. Can’t believe I missed that. Hopefully someone will learn from my idiocy.
June 4th, 2012 at 12:22
UIView is a subclass of UIResponder. So [[aView class] isSubclassOfClass:[UIResponder class]] will always be true.