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.