Don’t Block the (Text)Box

Sometimes the location where the user is trying to type on the screen is directly underneath where the on-screen keyboard will appear. Apple has provided a solution to this, but it doesn’t work out-of-the-box these days. With iOS 8 and custom keyboard extensions just around the corner, it’d be a good idea to take a look at how you prevent view obstruction in your apps.Don't Block the Box

First things first, your text field should be within a UIScrollView. There are other ways to fix the problem, but a UIScrollView is the easiest. Then, simply check that the bottom of the view you are typing into is visible in the non-obstructed section of the view.

Start by having your UIViewController listen for the NSNotification that the keyboard has been displayed and will be hidden. Some properties we will need include:

@property (nonatomic, assign) BOOL scrollViewInitialValuesSaved;
@property (nonatomic, assign) UIEdgeInsets scrollViewInsets;
@property (nonatomic, assign) UIEdgeInsets scrollViewIndicatorInsets;
@property (nonatomic, assign) CGPoint scrollViewOffset;
 
@property (nonatomic, weak) IBOutlet UIScrollView *scrollView
- (void)viewDidLoad
{
	[super viewDidLoad];
	self.scrollViewInitialValuesSaved = NO;
}
 
- (void)viewWillAppear:(BOOL)animated
{
	[super viewWillAppear:animated];
 
	[[NSNotificationCenter defaultCenter] addObserver:self
						 selector:@selector(keyboardWasShown:)
						     name:UIKeyboardDidShowNotification
						   object:nil];
	[[NSNotificationCenter defaultCenter] addObserver:self
						 selector:@selector(keyboardWillBeHidden:)
						     name:UIKeyboardWillHideNotification
						   object:nil];
}
 
- (void)viewWillDisappear:(BOOL)animated
{
	[super viewWillDisappear:animated];
 
	[[NSNotificationCenter defaultCenter] removeObserver:self
						        name:UIKeyboardDidShowNotification
						      object:nil];
	[[NSNotificationCenter defaultCenter] removeObserver:self
						        name:UIKeyboardWillHideNotification
						      object:nil];
}

Then, the interesting part.

- (void)keyboardWasShown:(NSNotification *)notification
{
	/*
	 * Save away the original values of the UIScrollView. These might not be 0,
	 * in the event that you have a UITabBar, UINavigationBar, etc.
	 */
	if (!self.scrollViewInitialValuesSaved) {
		self.scrollViewInsets = self.scrollView.contentInset;
		self.scrollViewIndicatorInsets = self.scrollView.scrollIndicatorInsets;
		self.scrollViewOffset = self.scrollView.contentOffset;
 
		self.scrollViewInitialValuesSaved = YES;
	}
 
	/* Look at the ending frame, not the beginning frame to adjust for iOS 8 QuickType bar */
	CGFloat keyboardHeight = [notification.userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue].size.height;
 
	/*
	 * Adjust the UIScrollView insets to account for the keyboard's height.
	 */
	self.scrollView.contentInset = UIEdgeInsetsMake(self.scrollViewInsets.top,
							self.scrollViewInsets.left,
							self.scrollViewInsets.bottom + keyboardHeight,
							self.scrollViewInsets.right);
	self.scrollView.scrollIndicatorInsets = UIEdgeInsetsMake(self.scrollViewIndicatorInsets.top,
								 self.scrollViewIndicatorInsets.left,
								 self.scrollViewIndicatorInsets.bottom + keyboardHeight,
								 self.scrollViewIndicatorInsets.right);
 
	/*
	 * Check if the keyboard is covering the bottom right corner of the control in question.
	 */
	CGRect contentRect = self.view.frame;
	contentRect.size.height -= keyboardHeight;
	CGPoint textFieldPoint = CGPointMake(CGRectGetMaxX(/* control */.frame), CGRectGetMaxY(/* control */.frame));
	/* The UIScrollView could have already been scrolled */
	textFieldPoint.y -= self.scrollView.contentOffset.y;
	if (!CGRectContainsPoint(contentRect, textFieldPoint)) {
		/* View is obstructed, have the UIScrollView scroll the view to visibility. */
		CGPoint scrollPoint = CGPointMake(0.0, CGRectGetMaxY(/* control */.frame) - contentRect.size.height);
		[self.scrollView setContentOffset:scrollPoint animated:YES];
	}
}
 
- (void)keyboardWillBeHidden:(NSNotification *)notification
{
	/*
	 * Back to the original resting position.
	 */
	[UIView animateWithDuration:0.3 animations:^() {
		self.scrollView.contentInset = self.scrollViewInsets;
		self.scrollView.scrollIndicatorInsets = self.scrollViewIndicatorInsets;
	}];
	[self.scrollView setContentOffset:self.scrollViewOffset animated:YES];
 
}

There’s a few things to note about where Apple’s solution went wrong:

  • The insets that get saved away could change at any point, especially if you leave a view and come back to it without dismissing the keyboard.
  • Only the top-left of the view was questioned as to whether or not the entire view is obstructed.
  • The implementation assumed that the UIScrollView had never been scrolled.
  • The height of the keyboard can change without redisplaying the keyboard under iOS 8, which makes the relative calculations based on UIKeyboardFrameBeginUserInfoKey incorrect.
  • Custom insets were ignored, which is problematic, especially considering UIViewController‘s
    automaticallyAdjustsScrollViewInsets
    property.

If you want to automatically track which UITextFields are being obstructed, remember that you can do so by assigning a property when a UITextField‘s textFieldDidBeginEditing: delegate method is called.

Leave a Reply

Your email address will not be published. Required fields are marked *