Scrollable form in iOS when the virtual keyboard is open

In our previous article we saw how to create a scrollable form in Android when the virtual keyboard is open. This article will show you the same in iOS.

Why would you need a scrollable form? Because modern mobile phones and tablets use virtual keyboards to let users type in input fields. The limited surface of the screen usually leaves a narrow space (viewport) for the input fields. iOS has no built-in ways to handle this, so this article fills the gap.

In principle, we need a UIScrollView in the background that will serve its inherent scroll features. Next, we will need a UIView as a container for the input controls. The idea is to scroll the container up and down according to the available space. The container also serves as a simple way to maintain the geometry of the form when the user rotates the device.

In Figure 1 you can see how your can use the IDE of Xcode to add a UIScrollView and then add contraints to the edges of the screen. Those constraints will come in use when the user rotates the device between portrait and landscape.

Use the panel on the right to find "Scroll View" and drag it in the screen area. Use the buttons on the bottom to open the contraints panel. Click the four red dotted lines to make them solid and click the "Add 4 Contraints" button. Now the scroll view is properly set.

Figure 1

The next step shown in Figure 2 is to add the container for our input fields. Use the panel on the right to find "View" and drag it in the editor, somewhere outside the screen. From the top right corner find the "Size" drop down box and set it to "Freeform". This way you can set a custom width and height of the container panel that fits the input fields we will add later.

Figure 2

Figure 3

Add some input controls in the container. Figure 3 is a sample of what you can do. One small tip is to leave a small gap on the bottom, it will be useful for helping users not mistakenly confuse your controls with the virtual keyboard when it is open.

After you are done with the input controls in the interface, lets add some IBOutlets to connect them with the code. The following code is the controller's header file (.h file). This article is interested in the scroll view and the container panel, but you should also add IBOutlet properties for the rest of your input fields. The property keyboard_h will be used in the implementation code.

#import <UIKit/UIKit.h>

@interface MainController : UIViewController

@property (nonatomic, retain) IBOutlet UIScrollView *scroll_view;
@property (nonatomic, retain) IBOutlet UIView *panel;
@property NSInteger keyboard_h;

@end

The following code is what makes everything happen. It is the code for your controller's implementation file (.m file).

#import "MainController.h"

@interface MainController ()

@end

@implementation MainController

@synthesize scroll_view, panel, keyboard_h;

- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil {
    self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
    if (self) {
        // Custom initialization
    }
    return self;
}

- (void)viewDidLoad {
    [super viewDidLoad];

    [scroll_view addSubview:panel];
    
    [[NSNotificationCenter defaultCenter] addObserver:self
        selector:@selector(keyboardWillShow:) name:UIKeyboardWillShowNotification object:nil];
    [[NSNotificationCenter defaultCenter] addObserver:self
        selector:@selector(keyboardWillHide:) name:UIKeyboardWillHideNotification object:nil];
    
    [self refresh_layout];
    scroll_view.contentOffset = CGPointMake(0, 0);
}

- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}

- (void)keyboardWillShow:(NSNotification *)notification {
    NSDictionary *info = [notification userInfo];
    CGRect rect = [info[UIKeyboardFrameEndUserInfoKey] CGRectValue];
    keyboard_h = UIDeviceOrientationIsLandscape(self.interfaceOrientation) ? rect.size.width : rect.size.height;
    [self refresh_layout];
}

- (void)keyboardWillHide:(NSNotification *)notification {
    keyboard_h = 0;
    [self refresh_layout];
}

- (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation {
    [self refresh_layout];
}

- (void)refresh_layout {
    CGSize size = [[UIScreen mainScreen] bounds].size;
    CGFloat window_width = UIDeviceOrientationIsLandscape(self.interfaceOrientation) ? size.height : size.width;
    
    panel.frame = CGRectMake(
        round((window_width - panel.frame.size.width) / 2),
        0,
        panel.frame.size.width,
        panel.frame.size.height
    );
    
    size = [[UIApplication sharedApplication] statusBarFrame].size;
    
    scroll_view.contentInset = UIEdgeInsetsMake(
        MIN(size.width, size.height),
        0,
        panel.frame.origin.y + panel.frame.size.height + keyboard_h,
        0
    );
}

@end

The layout calculations are performed in the method -(void)refresh_layout. The rest of the unit's methods call refresh_layout responding to the keyboard's open/close events or on rotating the device.

The way this unit is built responds nicely even when the virtual keyboard never opens, but the form is too long to appear propery when the device is in landscape view.

That was it, your iOS app now works nicely with virtual keyboards and long forms.