UITextField docked like iOS Messenger

A while ago I was searching for code on how to create a textbar that is docked at the bottom of the screen and moves with the keyboard when the textbox is selected. Very similar behavior to what is in iOS Messenger.

You can view a full working example on GitHub: https://github.com/bmancini55/iOSExamples-DockedKeyboardView

Using InputAccessoryView

Cocoa documentation often requires a scavenger hunt to build a full picture of what you're trying to accomplish. This example is no exception.

In my search, I stumbled upon the Custom Views for Data Input guide. This caught my eye...

You may also attach an input accessory view to the system keyboard or to a custom input view; this accessory view runs along the top of the main input view and can contain, for example, controls that affect the text in some way or labels that display some information about the text.

That sounds like what we're after. A bit more info...

Any class inheriting directly or indirectly from UIResponder (usually a custom view) can specify its own input view and input accessory view.

UIView and UITableView are all subclasses of UIResponder and include properties for inputView and inputAccessoryView.

A little more reading reveals this...

When the responder object becomes the first responder and inputView (or inputAccessoryView) is not nil, UIKit animates the input view into place below the parent view (or attaches the input accessory view to the top of the input view).

Now imagine we attach this to the View or TableView in your ViewController! Our inputAccessView will automatically be animated into a docked position at the bottom if the View becomes the first responder. Furthermore it will automatically be shown at the top of the keyboard when the keyboard moves.

This is exactly what we need! Reading a bit more includes information about how to implement these propeties:

inputAccessoryView The custom accessory view to display when the object becomes the first responder. (read-only)

@property(readonly, retain) UIView *inputAccessoryView Discussion

The default value of this property is nil. Subclasses that want to attach custom controls to either a system-supplied input view (such as the keyboard) or a custom input view (one you provide in the inputView property) should redeclare this property as readwrite and use it to manage their custom accessory view. When the receiver subsequently becomes the first responder, the responder infrastructure attaches the view to the appropriate input view before displaying it.

This property is typically used to attach an accessory view to the system-supplied keyboard that is presented for UITextField and UITextView objects.

There are two main things here:

  1. We need to override the inputAccessoryView property to use it
  2. This view will automatically be displayed when our view becomes the first responder
Step 1: Create the View for Docking

Let's get started by creating a view that we want to be docked. In this example, it will contain a single UITextView, nothing fancy.

@interface KeyboardBar : UIView
@property (strong, nonatomic) UITextView *textView;
@end

@implementation KeyboardBar

- (id)init {
    CGRect screen = [[UIScreen mainScreen] bounds];
    CGRect frame = CGRectMake(0,0, CGRectGetWidth(screen), 40);
    self = [self initWithFrame:frame];
    return self;
}

- (id)initWithFrame:(CGRect)frame {

    self = [super initWithFrame:frame];
    if(self) {

        self.backgroundColor = [UIColor colorWithWhite:0.75f alpha:1.0f];

        self.textView = [[UITextView alloc]initWithFrame:CGRectInset(frame, 10, 5)];
        self.textView.backgroundColor = [UIColor colorWithWhite:1.0f alpha:1.0f];
        [self addSubview:self.textView];
    }
    return self;
}

@end
Step 2: Create a custom view that overrides inputAccessoryView

We now need to implement the inputAccessoryView property. We do this by creating a custom UIView and overriding the property as a readwrite property.

Then, in the inputAccessoryView getter you can reference your KeyboardBar view that you want to be docked. In this example, it will lazy initialize an instance of KeyboardBar on first access.

#import "CustomView.h"
#import "KeyboardBar.h"

@interface CustomView()

// Override inputAccessoryView to readWrite
@property (nonatomic, readwrite, retain) UIView *inputAccessoryView;

@end

@implementation CustomView

// Override canBecomeFirstResponder
// to allow this view to be a responder
- (bool) canBecomeFirstResponder {
    return true;
}

// Override inputAccessoryView to use
// an instance of KeyboardBar
- (UIView *)inputAccessoryView {
    if(!_inputAccessoryView) {
        _inputAccessoryView = [[KeyboardBar alloc] init];
    }
    return _inputAccessoryView;
}

@end
Step 3: Use your custom view

Finally you need to use your custom view in your controller. You do this by setting the view property in the loadView method of the controller.

We also call becomeFirstResponder on the view. This is what triggers the inputAccessoryView to display when the controller loads.

#import "ViewController.h"
#import "CustomView.h"

@interface ViewController ()

@property (strong, nonatomic) CustomView *view;

@end

@implementation ViewController

// Override loadView so we can use CustomView that implements
// inputAccessoryView. Also set the view as the first responder
// so that it displays the inputAccessoryView on load.
- (void)loadView {
    self.title = @"View";

    // Use your CustomView instead of the UIView that is normally attached with [super loadView]
    self.view = [[CustomView alloc]initWithFrame:[UIScreen mainScreen].bounds];
    self.view.backgroundColor = [UIColor whiteColor];
    [self.view becomeFirstResponder];

    // Add a TapGestureRecognizer to dismiss the keyboard when the view is tapped
    UITapGestureRecognizer *recognizer = [[UITapGestureRecognizer alloc]initWithTarget:self action:@selector(didTouchView)];
    [self.view addGestureRecognizer:recognizer];
}


// Dissmiss the keyboard on view touches by making
// the view the first responder
- (void)didTouchView {
    [self.view becomeFirstResponder];
}

Important Note the use of [self.view becomeFirstResponder] calls is very important. This is what triggers the display of inputAccessoryView. If you're not seeing it, you need to make sure you're calling becomeFirstResponder on the view.

That's all there is to it. When your controller load, it will load the KeyboardBar at the bottom of the view.

WARNING the previous example of this code overrode inputAccessoryView directly on the controller. While this worked in iOS7, it no longer works in iOS8. When the controller is pushed onto the Navigation stack, it will cause a retain cycle and prevent the view from being released. The workaround I came up with, and demonstrated in the one in this article. This keeps the exact same functionality without causing memory leaks.

Working Example

You can view a full working example on GitHub: https://github.com/bmancini55/iOSExamples-DockedKeyboardView

Alternative Technique: Using Manual Animation

Another approach to adding the KeyboardBar directly to your ViewController's view. You can then use KeyboardNotification events to move the KeyboardBar around the screen.

@interface KeyboardBarView : UIView
@property (strong, nonatomic) UITextView *textView;
@end

The implementation creates the components on initialization and uses NotificationCenter to watch for UIKeyboardWillShowNotification and UIKeyboardWillHideNotification events to perform the animation.

The complicated part of this is how we handle these events. In the code below, we calculate the movement of the Keyboard and construct a corresponding animation.

#import "KeyboardBarView.h"

@implementation KeyboardBarView

const int MARGIN_SIZE = 5;  
const int BUTTON_WIDTH = 52;

-(id)initWithFrame:(CGRect)frame 
{
    self = [super initWithFrame:frame];
    if (self != nil)
    {
        // create the textbox
        self.textView = [[UITextView alloc] init];
        self.textView.frame = CGRectMake(40, MARGIN_SIZE, self.frame.size.width - 100, self.frame.size.height - (2 * MARGIN_SIZE));
        self.textView.editable = true;
        self.textView.keyboardType = UIKeyboardTypeDefault;
        self.textView.backgroundColor = [UIColor whiteColor];
        [self addSubview:self.textView];

        // create hooks for keyboard
        [[NSNotificationCenter defaultCenter] addObserver:self
                                                 selector:@selector(keyboardDidShowOrHide:)
                                                     name:UIKeyboardWillShowNotification
                                                   object:nil];
        [[NSNotificationCenter defaultCenter] addObserver:self
                                                 selector:@selector(keyboardDidShowOrHide:)
                                                     name:UIKeyboardWillHideNotification
                                                   object:nil];
    }
    return self;
}

- (void)dealloc {
    [[NSNotificationCenter defaultCenter] removeObserver:self];
}

#pragma mark - Keyboard methods

-(void)keyboardDidShowOrHide:(NSNotification *)notification
{
    NSDictionary *userInfo = [notification userInfo];
    NSTimeInterval animationDuration;
    UIViewAnimationCurve animationCurve;
    CGRect keyboardEndFrame;

    [[userInfo objectForKey:UIKeyboardAnimationCurveUserInfoKey] getValue:&animationCurve];
    [[userInfo objectForKey:UIKeyboardAnimationDurationUserInfoKey] getValue:&animationDuration];
    [[userInfo objectForKey:UIKeyboardFrameEndUserInfoKey] getValue:&keyboardEndFrame];

    [UIView beginAnimations:nil context:nil];
    [UIView setAnimationDuration:animationDuration];
    [UIView setAnimationCurve:animationCurve];

    CGRect newFrame = self.frame;
    newFrame.origin.y = keyboardEndFrame.origin.y - newFrame.size.height;
    self.frame = newFrame;

    [UIView commitAnimations];
}
@end

As you can see, that's a lot of code to simply make the view move in sync with the Keyboard.

We can add it to a ViewController and it will take care of the necessary actions.

comments powered by Disqus