ColinE
ColinE

Reputation: 70122

How to re-size UITextView when keyboard shown with iOS 7

I have a view controller which contains a full-screen UITextView. When the keyboard is shown I would like to resize the text view so that it is not hidden under the keyboard.

This is a fairly standard approach with iOS, as described in this question:

How to resize UITextView on iOS when a keyboard appears?

However, with iOS 7, if the user taps on the text view in the bottom half of the screen, when the text view resizes, the cursor remains offscreen. The text view only scrolls to bring the cursor into view if when the user hits enter.

Upvotes: 14

Views: 24344

Answers (8)

Johnston
Johnston

Reputation: 20854

I read the docs which talk about this very topic. I translated it into Swift and it worked absolutely beautifully for me.

This is used for a full page UITextView like iMessage.

I am using iOS 8.2 and Swift on XCode 6.2 and here's my code. Just call this setupKeyboardNotifications from your viewDidLoad or other initialization method.

func setupKeyboardNotifications() {
    NSNotificationCenter.defaultCenter().addObserver(self, selector: Selector("keyboardWasShown:"), name: UIKeyboardDidShowNotification, object: nil)
    NSNotificationCenter.defaultCenter().addObserver(self, selector: Selector("keyboardWillBeHidden:"), name: UIKeyboardWillHideNotification, object: nil)
}

func keyboardWasShown(aNotification:NSNotification) {
    let info = aNotification.userInfo
    let infoNSValue = info![UIKeyboardFrameBeginUserInfoKey] as NSValue
    let kbSize = infoNSValue.CGRectValue().size
    let contentInsets = UIEdgeInsetsMake(0.0, 0.0, kbSize.height, 0.0)
    codeTextView.contentInset = contentInsets
    codeTextView.scrollIndicatorInsets = contentInsets
}

func keyboardWillBeHidden(aNotification:NSNotification) {
    let contentInsets = UIEdgeInsetsZero
    codeTextView.contentInset = contentInsets
    codeTextView.scrollIndicatorInsets = contentInsets
}

Also if you are having issues with the caret being in the right place when rotated check for the orientation change and scroll to the right position.

override func didRotateFromInterfaceOrientation(fromInterfaceOrientation: UIInterfaceOrientation) {
    scrollToCaretInTextView(codeTextView, animated: true)
}

func scrollToCaretInTextView(textView:UITextView, animated:Bool) {
    var rect = textView.caretRectForPosition(textView.selectedTextRange?.end)
    rect.size.height += textView.textContainerInset.bottom
    textView.scrollRectToVisible(rect, animated: animated)
}

Swift 3:

func configureKeyboardNotifications() {
    NotificationCenter.default.addObserver(self, selector: #selector(keyboardWasShown(aNotification:)), name: NSNotification.Name.UIKeyboardDidShow, object: nil)
    NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillBeHidden(aNotification:)), name: NSNotification.Name.UIKeyboardWillHide, object: nil)
}

func keyboardWasShown(aNotification:NSNotification) {
    let info = aNotification.userInfo
    let infoNSValue = info![UIKeyboardFrameBeginUserInfoKey] as! NSValue
    let kbSize = infoNSValue.cgRectValue.size
    let contentInsets = UIEdgeInsetsMake(0.0, 0.0, kbSize.height, 0.0)
    textView.contentInset = contentInsets
    textView.scrollIndicatorInsets = contentInsets
}

func keyboardWillBeHidden(aNotification:NSNotification) {
    let contentInsets = UIEdgeInsets.zero
    textView.contentInset = contentInsets
    textView.scrollIndicatorInsets = contentInsets
}

Swift 4 & 5:

func setupKeyboardNotifications() {
    NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow(_ :)), name: UIResponder.keyboardWillShowNotification, object: nil)
    NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide(_:)), name: UIResponder.keyboardWillHideNotification, object: nil)
}



@objc func keyboardWillShow(_ notification:NSNotification) {
    let d = notification.userInfo!
    var r = (d[UIResponder.keyboardFrameEndUserInfoKey] as! NSValue).cgRectValue
    r = self.textView.convert(r, from:nil)
    self.textView.contentInset.bottom = r.size.height
    self.textView.verticalScrollIndicatorInsets.bottom = r.size.height

}

@objc func keyboardWillHide(_ notification:NSNotification) {
    let contentInsets = UIEdgeInsets.zero
    self.textView.contentInset = contentInsets
    self.textView.verticalScrollIndicatorInsets = contentInsets
}

Upvotes: 27

jrc
jrc

Reputation: 21881

@Johnston found a good solution. Here's a variation using UIKeyboardWillChangeFrameNotification which correctly accounts for keyboard size changes (i.e. showing/hiding the QuickType bar). It also correctly handles the case where the text view is embedded in a navigation controller (i.e. where the contentInset isn't otherwise zero). It's also written in Swift 2.

override func viewDidLoad() {
    :

    NSNotificationCenter.defaultCenter().addObserverForName(UIKeyboardWillChangeFrameNotification, object: nil, queue: nil) { (notification) -> Void in
        guard let userInfo = notification.userInfo,
            let keyboardFrameEndValue = userInfo[UIKeyboardFrameEndUserInfoKey] as? NSValue
            else { return }

        let windowCoordinatesKeyboardFrameEnd = keyboardFrameEndValue.CGRectValue() // window coordinates
        let keyboardFrameEnd = self.view.convertRect(windowCoordinatesKeyboardFrameEnd, fromView: nil) // view coordinates

        var inset = self.textView.contentInset
        inset.bottom = CGRectGetMaxY(self.textView.frame) - CGRectGetMinY(keyboardFrameEnd) // bottom inset is the bottom of textView minus top of keyboard
        self.textView.contentInset = inset
        self.textView.scrollIndicatorInsets = inset
    }
}

Upvotes: 0

barko
barko

Reputation: 1

This is my solution, July 2015 using Swift 1.2 on Xcode 6.4 targeting iOS 7.1 - a combination of several approaches. Borrowed Johnston's keyboard handing Swift code. Its a bit of a hack, but its simple and it works.

I have a vanilla UITextView inside a single View.

I did not want to embed it inside a UIScrollView as per Apple's documentation. I just wanted the UITextView re-sized when software keyboard appeared, and resized to original when keyboard was dismissed.

These are the basic steps:

  1. Set up keyboard notifications
  2. Set up layout constraint in "Interface Builder" (TextView to bottom edge in my case)
  3. Create an IBOutlet for this constraint in the relevant code file so you can adjust it programmatically
  4. Use keyboard notifications to intercept events and get keyboard size
  5. Programmatically adjust constraint IBOutlet using keyboard size to re-size TextView.
  6. Put everything back when keyboard is dismissed.

So, onto the code.

I've set up constraint outlet at the top of the code file via the usual drag-drop in interface builder: @IBOutlet weak var myUITextViewBottomConstraint: NSLayoutConstraint!

I also set up a global variable where I can back up the state of affairs before the keyboard come up: var myUITextViewBottomConstraintBackup: CGFloat = 0

Implement keyboard notifications, call this function in viewDidLoad or any other startup/setup section:

func setupKeyboardNotifications() {

    NSNotificationCenter.defaultCenter().addObserver(self, selector: Selector("keyboardWasShown:"), name: UIKeyboardDidShowNotification, object: nil)

    NSNotificationCenter.defaultCenter().addObserver(self, selector: Selector("keyboardWillBeHidden:"), name: UIKeyboardWillHideNotification, object: nil)

    }

Then these two functions will be called automatically when keyboard is shown/dismissed:

func keyboardWasShown(aNotification:NSNotification) {

    let info = aNotification.userInfo
    let infoNSValue = info![UIKeyboardFrameBeginUserInfoKey] as! NSValue
    let kbSize = infoNSValue.CGRectValue().size

    let newHeight = kbSize.height

    //backup old constraint size
    myUITextViewBottomConstraintOld = myUITextViewBottomConstraint.constant 

    // I subtract 50 because otherwise it leaves a gap between keyboard and text view. I'm sure this could be improved on.
    myUITextViewBottomConstraint.constant = newHeight - 50 

func keyboardWillBeHidden(aNotification:NSNotification) {
    //restore to whatever AutoLayout set it before you messed with it
    myUITextViewBottomConstraint.constant = myUITextViewBottomConstraintOld 

}

The code works, with a minor issue:

  • It's not responsive to the predictive text ribbon above the keyboard opening/closing. I.e. it will take the state of it into account when the keyboard is called up, but if you were to slide it up or down while keyboard is shown the constraint will not be adjusted. It is a separate event that needs to be handled. Its not enough of a functionality hit for me to bother with.

Upvotes: 0

Patel Jigar
Patel Jigar

Reputation: 2151

i had done it and its work completely.

  #define k_KEYBOARD_OFFSET 95.0

-(void)keyboardWillAppear {
    // Move current view up / down with Animation
    if (self.view.frame.origin.y >= 0)
    {
        [self moveViewUp:NO];
    }
    else if (self.view.frame.origin.y < 0)
    {
        [self moveViewUp:YES];
    }
}

-(void)keyboardWillDisappear {
    if (self.view.frame.origin.y >= 0)
    {
        [self moveViewUp:YES];
    }
    else if (self.view.frame.origin.y < 0)
    {
        [self moveViewUp:NO];
    }
}

-(void)textFieldDidBeginEditing:(UITextField *)sender
{
    //if ([sender isEqual:_txtPassword])
   // {
        //move the main view up, so the keyboard will not hide it.
        if  (self.view.frame.origin.y >= 0)
        {
            [self moveViewUp:YES];
        }
    //}
}

//Custom method to move the view up/down whenever the keyboard is appeared / disappeared
-(void)moveViewUp:(BOOL)bMovedUp
{
    [UIView beginAnimations:nil context:NULL];
    [UIView setAnimationDuration:0.4]; // to slide the view up

    CGRect rect = self.view.frame;
    if (bMovedUp) {
        // 1. move the origin of view up so that the text field will come above the keyboard
        rect.origin.y -= k_KEYBOARD_OFFSET;

        // 2. increase the height of the view to cover up the area behind the keyboard
        rect.size.height += k_KEYBOARD_OFFSET;
    } else {
        // revert to normal state of the view.
        rect.origin.y += k_KEYBOARD_OFFSET;
        rect.size.height -= k_KEYBOARD_OFFSET;
    }

    self.view.frame = rect;

    [UIView commitAnimations];
}

- (void)viewWillAppear:(BOOL)animated
{
    // register keyboard notifications to appear / disappear the keyboard
    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(keyboardWillAppear)
                                                 name:UIKeyboardWillShowNotification
                                               object:nil];

    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(keyboardWillDisappear)
                                                 name:UIKeyboardWillHideNotification
                                               object:nil];
}

- (void)viewWillDisappear:(BOOL)animated
{
    // unregister for keyboard notifications while moving to the other screen.
    [[NSNotificationCenter defaultCenter] removeObserver:self
                                                    name:UIKeyboardWillShowNotification
                                                  object:nil];

    [[NSNotificationCenter defaultCenter] removeObserver:self
                                                    name:UIKeyboardWillHideNotification
                                                  object:nil];
}

Upvotes: 0

Thomas Tempelmann
Thomas Tempelmann

Reputation: 12043

With Auto Layout, it's much easier (provided you understand Auto Layout) to handle:

Instead of trying to identify and resize the affected views, you simply create a parent frame for all your view's contents. Then, if the kbd appears, you resize the frame, and if you've set up the constraints properly, the view will re-arrange all its child views nicely. No need to fiddle with lots of hard-to-read code for this.

In fact, in a similar question I found a link to this excellent tutorial about this technique.

Also, the other examples here that do use textViewDidBeginEditing instead of the UIKeyboardWillShowNotification have one big issue:

If the user has an external bluetooth keyboard attached then the control would still get pushed up even though no on-screen keyboard appears. That's not good.

So, to summarize:

  1. Use Auto Layout
  2. Use the UIKeyboardWillShowNotification notification, not the TextEditField's events for deciding when to resize your views.

Alternatively, check out LeoNatan's reply. That might even be a cleaner and simpler solution (I've not tried myself yet).

Upvotes: 4

ColinE
ColinE

Reputation: 70122

Whilst the answer given by @Divya lead me to the correct solution (so I awarded the bounty), it is not a terribly clear answer! Here it is in detail:

The standard approach to ensuring that a text view is not hidden by the on-screen keyboard is to update its frame when the keyboard is shown, as detailed in this question:

How to resize UITextView on iOS when a keyboard appears?

However, with iOS 7, if you change the text view frame within your handler for the UIKeyboardWillShowNotification notification, the cursor will remain off screen as described in this question.

The fix for this issue is to change the text view frame in response to the textViewDidBeginEditing delegate method instead:

@implementation ViewController {
    CGSize _keyboardSize;
    UITextView* textView;
}

- (void)viewDidLoad
{
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.

    textView = [[UITextView alloc] initWithFrame:CGRectInset(self.view.bounds, 20.0, 20.0)];    textView.delegate = self;
    textView.returnKeyType = UIReturnKeyDone;
    textView.backgroundColor = [UIColor greenColor];
    textView.textColor = [UIColor blackColor];
    [self.view addSubview:textView];


    NSMutableString *textString = [NSMutableString new];
    for (int i=0; i<100; i++) {
        [textString appendString:@"cheese\rpizza\rchips\r"];
    }
    textView.text = textString;

}

- (void)textViewDidBeginEditing:(UITextView *)textView1 {
    CGRect textViewFrame = CGRectInset(self.view.bounds, 20.0, 20.0);
    textViewFrame.size.height -= 216;
    textView.frame = textViewFrame;
}

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
    CGRect textViewFrame = CGRectInset(self.view.bounds, 20.0, 20.0);
    textView.frame = textViewFrame;
    [textView endEditing:YES];
    [super touchesBegan:touches withEvent:event];
}

@end

NOTE: unfortunately textViewDidBeginEdting fires before the UIKeyboardWillShowNotification notification, hence the need to hard-code the keyboard height.

Upvotes: 3

Divya Bhaloidiya
Divya Bhaloidiya

Reputation: 5064

Following on is working for me :

.h file

@interface ViewController : UIViewController <UITextViewDelegate> {

    UITextView *textView ;

}

@property(nonatomic,strong)IBOutlet UITextView *textView;

@end

.m file

@implementation ViewController
@synthesize textView;
- (void)viewDidLoad
{
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.

    CGRect textViewFrame = CGRectMake(20.0f, 20.0f, 280.0f, 424.0f);
    //UITextView *textView = [[UITextView alloc] initWithFrame:textViewFrame];
    textView.frame = textViewFrame;
    textView.delegate = self;
    textView.returnKeyType = UIReturnKeyDone;
    textView.backgroundColor = [UIColor greenColor];
    textView.textColor = [UIColor blackColor];
    [self.view addSubview:textView];

}
- (BOOL)textViewShouldBeginEditing:(UITextView *)textView{
    NSLog(@"textViewShouldBeginEditing:");
    return YES;
}
- (void)textViewDidBeginEditing:(UITextView *)textView1 {
    NSLog(@"textViewDidBeginEditing:");
   CGRect textViewFrame = CGRectMake(20.0f, 20.0f, 280.0f, 224.0f);

    textView1.frame = textViewFrame;

}
- (BOOL)textViewShouldEndEditing:(UITextView *)textView{
    NSLog(@"textViewShouldEndEditing:");
       return YES;
}
- (void)textViewDidEndEditing:(UITextView *)textView{
    NSLog(@"textViewDidEndEditing:");
}
- (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text{
       return YES;
}

- (void)textViewDidChange:(UITextView *)textView{
    NSLog(@"textViewDidChange:");
}

- (void)textViewDidChangeSelection:(UITextView *)textView{
    NSLog(@"textViewDidChangeSelection:");
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
    NSLog(@"touchesBegan:withEvent:");
    CGRect textViewFrame = CGRectMake(20.0f, 20.0f, 280.0f, 424.0f);

    textView.frame = textViewFrame;
    [self.view endEditing:YES];
    [super touchesBegan:touches withEvent:event];
}
@end

Upvotes: 2

L&#233;o Natan
L&#233;o Natan

Reputation: 57040

Do not resize the text view. Instead, set the contentInset and scrollIndicatorInsets bottom to the keyboard height.

See my answer here: https://stackoverflow.com/a/18585788/983912


Edit

I made the following changes to your sample project:

- (void)textViewDidBeginEditing:(UITextView *)textView
{
    _caretVisibilityTimer = [NSTimer scheduledTimerWithTimeInterval:0.3 target:self selector:@selector(_scrollCaretToVisible) userInfo:nil repeats:YES];
}

- (void)_scrollCaretToVisible
{
    //This is where the cursor is at.
    CGRect caretRect = [self.textView caretRectForPosition:self.textView.selectedTextRange.end];

    if(CGRectEqualToRect(caretRect, _oldRect))
        return;

    _oldRect = caretRect;

    //This is the visible rect of the textview.
    CGRect visibleRect = self.textView.bounds;
    visibleRect.size.height -= (self.textView.contentInset.top + self.textView.contentInset.bottom);
    visibleRect.origin.y = self.textView.contentOffset.y;

    //We will scroll only if the caret falls outside of the visible rect.
    if(!CGRectContainsRect(visibleRect, caretRect))
    {
        CGPoint newOffset = self.textView.contentOffset;

        newOffset.y = MAX((caretRect.origin.y + caretRect.size.height) - visibleRect.size.height + 5, 0);

        [self.textView setContentOffset:newOffset animated:NO];
    }
}

Removed setting old caret position at first, as well as disabled animation. Now seems to work well.

Upvotes: 3

Related Questions