16

I have a UITextView sitting on top of a UIView, and if I tap on it to open it for editing, then the keyboard is blocking the bottom of the view and I can not see it even though I can write in this area. Can I tell the UITextView to have a different scroll area or what is the solution?

Neigaard
  • 3,726
  • 9
  • 49
  • 85
  • 1
    duplicate look here for answer http://stackoverflow.com/questions/1126726/how-to-make-a-uitextfield-move-up-when-keyboard-is-present – Aaron Saunders Sep 29 '10 at 21:14

9 Answers9

33

A better solution, specially for iOS 7, would be to adjust the content inset property of the textview instead of its frame, this way, the keyboard will blur the text that falls behinds it like in any other iOS 7 app. You'll also have to adjust the scroll indicators to match.

Expanding Lindemann's answer,

- (void)keyboardWasShown:(NSNotification*)notification {
    NSDictionary* info = [notification userInfo];
    CGSize keyboardSize = [[info objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue].size;

    self.textView.contentInset = UIEdgeInsetsMake(0, 0, keyboardSize.height, 0);
    self.textView.scrollIndicatorInsets = self.textView.contentInset;
}

- (void)keyboardWillBeHidden:(NSNotification*)notification {
    self.textView.contentInset = UIEdgeInsetsZero;
    self.textView.scrollIndicatorInsets = UIEdgeInsetsZero;
}
Mike Morearty
  • 9,953
  • 5
  • 31
  • 35
Alejandro
  • 367
  • 3
  • 10
  • Thank you for this answer. This should absolutely be marked as the "correct" answer, especially, as you mention, given the changes in iOS 7. – Mani Aug 28 '13 at 21:15
  • In iOS 8, this seems to happen automatically – adamF Jul 10 '15 at 19:25
14

An easy solution is to implement the UITextViewDelegate Methods

- (void)textViewDidBeginEditing:(UITextView *)textView

and

- (void)textViewDidEndEditing:(UITextView *)textView

You can make the UITextView Frame smaller when the keyboard appears and make it full size again when the keyboard disappears...like this:

- (void)textViewDidBeginEditing:(UITextView *)textView {
    self.textView.frame = CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height/1.8);
}

- (void)textViewDidEndEditing:(UITextView *)textView {
    self.textView.frame = CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height);
}

EDIT

The solution above is bad...don't use it!!!

Now I think it's a better idea to resize the UITextView in proportion to the keyboard size and not with a fixed value...because the size of the keyboard can change when an other language become chosen or the device become rotated...of course -.-

At first you must register your UIViewController which displays your UITextView for receiving Keyboard Notifications:

- (void)viewDidLoad {
    [super viewDidLoad];

    [[NSNotificationCenter defaultCenter] addObserver:self
                                                 selector:@selector(keyboardWasShown:)
                                                     name:UIKeyboardDidShowNotification object:nil];

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

Then you have to implement the two methods -keyboardWasShown: and -keyboardWillBeHidden:.

The size of the actual keyboard is contained in the NSNotification object.

- (void)keyboardWasShown:(NSNotification*)notification {
    NSDictionary* info = [notification userInfo];
    CGSize keyboardSize = [[info objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue].size;
    self.textView.frame = CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height - keyboardSize.height);
}

- (void)keyboardWillBeHidden:(NSNotification*)notification {
    self.textView.frame = CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height);
}
Lindemann
  • 3,336
  • 3
  • 29
  • 27
5

If you want a Messages App style input, you can use a nested UITextView (allows for multiple lines of text). It will look like this in the end:

enter image description here

You start by laying out a view to hold all the child views. Here the background colour of the bottomView is set to match UIKeyboardAppearanceDark. It rests at the bottom of the screen.

bottomView = [UIView new];
bottomView.frame = CGRectMake(0, h-45, w, 45);
bottomView.backgroundColor = [UIColor colorWithRed:0.078 green:0.078 blue:0.078 alpha:1];
[self.view addSubview:bottomView];

Then, add in a simple background view styled like a typical UITextField, and add the UITextView as a subview to that. The inputTV (UITextView) takes its height based upon the size of the font. Also, all the padding is removed from inputTV using the textContainer variables.

inputTVBG = [UIImageView new];
inputTVBG.frame = CGRectMake(10, 8, w-90, 29);
inputTVBG.backgroundColor = [[UIColor whiteColor] colorWithAlphaComponent:0.1f];
inputTVBG.layer.cornerRadius = 4.0f;
inputTVBG.userInteractionEnabled = true;
inputTVBG.clipsToBounds = true;
[bottomView addSubview:inputTVBG];

inputTV = [UITextView new];
inputTV.font = [UIFont systemFontOfSize:14.0f];
inputTV.frame = CGRectMake(5, 6, w-100, inputTV.font.lineHeight);
inputTV.backgroundColor = [UIColor clearColor];
inputTV.keyboardAppearance = UIKeyboardAppearanceDark;
inputTV.delegate = self;
inputTV.autocorrectionType = UITextAutocorrectionTypeNo;
inputTV.tintColor = [UIColor whiteColor];
inputTV.textColor = [UIColor whiteColor];
inputTV.textContainer.lineFragmentPadding = 0;
inputTV.textContainerInset = UIEdgeInsetsZero;
[inputTVBG addSubview:inputTV];

In the example above, I've included a label indicating how many letters are left (max / min characters) and a submit button.

lettersLeftLabel = [UILabel new];
lettersLeftLabel.frame = CGRectMake(w-70, 8, 60, 16);
lettersLeftLabel.font = [UIFont systemFontOfSize:12.0f];
lettersLeftLabel.textColor = [[UIColor whiteColor] colorWithAlphaComponent:0.5f];
lettersLeftLabel.alpha = 0.0f;
[bottomView addSubview:lettersLeftLabel];

submitButton = [UIButton new];
submitButton.frame = CGRectMake(w-70, 0, 60, 45);
[submitButton setTitle:@"SUBMIT" forState:UIControlStateNormal];
[submitButton setTitleColor:[_peacock.applePink colorWithAlphaComponent:0.5f] forState:UIControlStateNormal];
[submitButton addTarget:self action:@selector(submit) forControlEvents:UIControlEventTouchUpInside];
[submitButton.titleLabel setFont:[UIFont boldSystemFontOfSize:14.0f]];
[bottomView addSubview:submitButton];

Add this line early on in your code, so you get keyboard change updates:

[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillShow:) name:UIKeyboardWillShowNotification object:nil];

It calls the method below when a user clicks on the inputTV. Here it sets the variable 'keyboardHeight' used later on.

-(void)keyboardWillShow:(NSNotification *)n {
    CGRect rect = [n.userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue];
    CGRect keyboardFrame = [self.view convertRect:rect fromView:nil];
    keyboardHeight = keyboardFrame.size.height;
    [self textViewDidChange:inputTV];
}

This is the main bit of code that takes care of all the movement and resizing of the inputTV.

-(void)textViewDidChange:(UITextView *)textView {

    //1. letters and submit button vars
    int numberOfCharacters = (int)textView.text.length;
    int minCharacters = 50;
    int maxCharacters = 400;
    int remainingCharacters = maxCharacters-numberOfCharacters;

    //2. if entered letters exceeds maximum, reset text and return
    if (remainingCharacters <= 0){
        textView.text = [textView.text substringToIndex:maxCharacters];
        numberOfCharacters = maxCharacters;
    }

    //3. set height vars
    inputTV.scrollEnabled = true;
    float textHeight = textView.contentSize.height;
    float lineHeight = roundf(textView.font.lineHeight);
    float additionalHeight = textHeight - lineHeight;
    float moveUpHeight = keyboardHeight + additionalHeight;

    //4. default letter colour is weak white
    UIColor * letterColour = [[UIColor whiteColor] colorWithAlphaComponent:0.5f];
    if (numberOfCharacters < minCharacters){ //minimum threshold not met
        lettersLeftLabel.text = [NSString stringWithFormat:@"%i",  minCharacters-numberOfCharacters];
        letterColour = [_peacock.applePink colorWithAlphaComponent:0.5f];
    } else { //within range
        lettersLeftLabel.text = [NSString stringWithFormat:@"%i/%i", numberOfCharacters, maxCharacters];
        if (remainingCharacters<5){ //increase alpha towards the end of range
            letterColour = [[UIColor whiteColor] colorWithAlphaComponent:1.0f - ((float)remainingCharacters/10)];
        }
    }

    //5. hide/show letter label based on textView height
    float letterAlpha = 0.0f; //default hide
    if (additionalHeight > 0){ letterAlpha = 1.0f; } //if multiline, show
    [UIView animateWithDuration:0.3f
                          delay:0.0f
                        options:UIViewAnimationOptionCurveEaseOut
                     animations:^{
                         lettersLeftLabel.alpha = letterAlpha;
                         lettersLeftLabel.textColor = letterColour;
                     }
                     completion:^(BOOL finished){
                     }];

    //6. update submit colour based on minimum threshold
    UIColor * submitColour = [_peacock.applePink colorWithAlphaComponent:0.5f];
    bool enableSubmit = false;
    if (numberOfCharacters >= minCharacters){
        submitColour = _peacock.applePink;
        enableSubmit = true;
    }
    [submitButton setEnabled:enableSubmit];
    [UIView animateWithDuration:0.3f
                          delay:0.0f
                        options:UIViewAnimationOptionCurveEaseOut
                     animations:^{
                         [submitButton setTitleColor:submitColour forState:UIControlStateNormal];
                     }
                     completion:^(BOOL finished){
                     }];



    //7. special case if you want to limit the frame size of the input TV to a specific number of lines
    bool shouldEnableScroll = false;
    int maxNumberOfLines = 5; //anything above this triggers the input TV to stay stationary and update its scroll
    int actualNumberOfLines = textHeight / textView.font.lineHeight;
    if (actualNumberOfLines >= maxNumberOfLines){ //recalculate vars for frames
        textHeight = maxNumberOfLines * lineHeight;
        additionalHeight = textHeight - lineHeight;
        moveUpHeight = keyboardHeight + additionalHeight;
        shouldEnableScroll = true;
    }

    //8. adjust frames of views
    inputTV.frame = CGRectMake(5, 6, w-100, textHeight); //update immediately (parent view clips to bounds)
    [UIView animateWithDuration:0.3f
                          delay:0.0f
                        options:UIViewAnimationOptionCurveEaseOut
                     animations:^{
                         bottomView.frame = CGRectMake(0, h-45-moveUpHeight, w, 45+additionalHeight);
                         inputTVBG.frame = CGRectMake(10, 8, w-90, lineHeight+additionalHeight+13);
                         submitButton.frame = CGRectMake(w-70, additionalHeight, 60, 45);
                     }
                     completion:^(BOOL finished){
                         inputTV.scrollEnabled = shouldEnableScroll; //default disable scroll here to avoid bouncing

                     }];


}

In the above method, this is what's happening:

  1. If you want to set a minimum or maximum number of characters, you can do so here. You pull the the number of characters and store as an integer, and calculate how many characters are left.

  2. If the user has reached the maximum number of characters, reset the textView text by stripping back to your max.

  3. These vars are used to calculate how much you need to move your bottomView up and also for the resizing its subviews.

  4. This method is just to change the colour / text of some UI elements. It's not strictly necessary.

  5. This method brings the lettersLeftLabel onto view if you're using that. It's not necessary either.

  6. This enables the submit button only if the minimum number of characters has been reached. It changes the colour as an indicator to the user.

  7. If you want to limit the growth of the inputTV and surrounding elements, you can include this bit of code. It requires you to set the maximum number of lines you want to show. If the user exceeds the max, scroll is reenabled for the inputTV, otherwise it defaults to false (important to stop it bouncing).

  8. This is the main resizing logic, moving the bottomView up and resizing its child views. The submit button needs to stay in the same position, so move it down as the bottomView grows.

NOTE: If you just want the barebones code, you only need to implement steps 3 and 8.

Johnny Rockex
  • 4,136
  • 3
  • 35
  • 55
4

@Alejandro above has the right idea, but his code does not work in landscape mode. I have amended his keyboardWasShown: method to work correctly in all orientations:

- (void)keyboardWasShown:(NSNotification *)notification {
    if (self.textView != nil) {
        NSDictionary* info = [notification userInfo];
        CGRect keyboardRect = [self.textView convertRect:[[info objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue] fromView:nil];
        CGSize keyboardSize = keyboardRect.size;        

        self.textView.contentInset = UIEdgeInsetsMake(0, 0, keyboardSize.height, 0);
        self.textView.scrollIndicatorInsets = self.textView.contentInset;
    }
}
Mani
  • 714
  • 1
  • 6
  • 8
4

Apple has some code samples that deal with this exact situation.

Kris Markel
  • 12,142
  • 3
  • 43
  • 40
  • I have implemented the example from the link, and it works but the scroll area for my UITextView becomes quite a lot to small (like 50 pixel). Any idea why? – Neigaard Sep 29 '10 at 21:40
  • Check the resizing masks in Interface Builder. I'm not sure what the right settings are, but they control how the text view responds to size changes of its parent. – Kris Markel Sep 29 '10 at 22:02
  • When you say resizing mask, then what do you mean? – Neigaard Sep 30 '10 at 11:05
  • It's also known as "springs and struts". If you select your view open up the size inspector in Interface Builder (command-3), it's the section labeled "Autosizing". – Kris Markel Sep 30 '10 at 13:22
  • Hm I am really getting a headache here. I can get it to work, as long as I do not rotate the iPhone, then it goes crazy. If I rotate the iPhone, the text gets totally out of sight and is unreachable. Any ideas here? Funny I would have thought that this would be handled by the SDK and easy for me :) – Neigaard Oct 01 '10 at 10:49
  • Ok I almost got it working now. I store the keyboard size and orientation when I show the keyboard, and use that to reset the UITextView on change of orientation. I still have one problem though. My problem with the UITextView getting to small can be fixed in portrait mode by disabling the UIViewAutoResizingFlexibleHeight but then it will be all wrong when it is rotated to landscape. Any ideas for this one? – Neigaard Oct 01 '10 at 12:43
  • None other than I've found dealing with resizing masks requires a lot of experimentation before you fully understand how the different options interact with view frame changes. If you have your layout in Interface Builder, you can use the little "rotate view" arrow in the upper right corner to see how your view will react to orientation changes. It helps to see exactly what effect changing the "springs and struts" will have. – Kris Markel Oct 01 '10 at 16:59
4

I finally got it working. Here is my solution, can you guys spot any errors in my design?

@synthesize textView = _textView;
@synthesize callbackViewController = _callbackViewController;


-(void)keyboardWasShown:(NSNotification*)aNotification {
    if(keyboardShown) {
        return;
    }

    NSDictionary *info = [aNotification userInfo];

    // Get the size of the keyboard.
    NSValue *aValue = [info objectForKey:UIKeyboardFrameBeginUserInfoKey];
    keyboardSize = [aValue CGRectValue].size;

    // Resize the scroll view (which is the root view of the window)
    CGRect viewFrame = [self.textView frame];

    orientationAtShown = orientation;

    if(orientation == UIInterfaceOrientationPortrait || orientation == UIInterfaceOrientationPortraitUpsideDown) {
        viewFrame.size.height -= keyboardSize.height;
    } else {
        viewFrame.size.height -= keyboardSize.width;
    }

    self.textView.frame = viewFrame;

    // Scroll the active text field into view.
    //CGRect textFieldRect = [activeField frame];
    [self.textView scrollRectToVisible:viewFrame animated:YES];

    keyboardShown = YES;
}

-(void)keyboardWasHidden:(NSNotification*)aNotification {
    if(!keyboardShown) {
        return;
    }

    // Reset the height of the scroll view to its original value
    CGRect viewFrame = [self.textView frame];
    if(orientationAtShown == UIInterfaceOrientationPortrait || orientationAtShown == UIInterfaceOrientationPortraitUpsideDown) {
        viewFrame.size.height += keyboardSize.height;
    } else {
        viewFrame.size.height += keyboardSize.width;
    }

    self.textView.frame = viewFrame;

    keyboardShown = NO;
}

-(void)registerForKeyboardNotifications {
    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(keyboardWasShown:)
                                                 name:UIKeyboardDidShowNotification object:nil];

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

-(void)viewWillAppear:(BOOL)animated {
    keyboardShown = NO;
    [self registerForKeyboardNotifications];
}

-(void)viewWillDisappear:(BOOL)animated {
    [[NSNotificationCenter defaultCenter] removeObserver:self name:UIKeyboardWillShowNotification object:nil];
}

// Override to allow orientations other than the default portrait orientation.
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation {
    if(keyboardShown) {
        [self keyboardWasHidden:nil];
    }

    orientation = interfaceOrientation;

    CGRect viewFrame = [self.textView frame];
    if(orientation == UIInterfaceOrientationPortrait || orientation == UIInterfaceOrientationPortraitUpsideDown) {
        if(viewFrame.size.width > viewFrame.size.height) {
            CGRect viewFrameFixed = CGRectMake(viewFrame.origin.x, viewFrame.origin.y, viewFrame.size.height, viewFrame.size.width);
            self.textView.frame = viewFrameFixed;
        }
    } else {
        if(viewFrame.size.width < viewFrame.size.height) {
            CGRect viewFrameFixed = CGRectMake(viewFrame.origin.x, viewFrame.origin.y, viewFrame.size.height, viewFrame.size.width);
            self.textView.frame = viewFrameFixed;
        }
    }


    // Return YES for supported orientations
    return YES;
}
Neigaard
  • 3,726
  • 9
  • 49
  • 85
2

Add Observer first in viewDidLoad.

   - (void)viewDidLoad {
    [super viewDidLoad];

    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(keyboardWasShown:)
                                                 name:UIKeyboardDidShowNotification object:nil];

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

}

Call the methods

- (void)keyboardWasShown:(NSNotification*)aNotification
{
NSDictionary* info = [aNotification userInfo];
CGSize kbSize = [[info objectForKey:UIKeyboardFrameBeginUserInfoKey] CGRectValue].size;

UIEdgeInsets contentInsets = UIEdgeInsetsMake(0.0, 0.0, kbSize.height, 0.0);
self.textView.contentInset = contentInsets;
self.textView.scrollIndicatorInsets = contentInsets;

// If active text field is hidden by keyboard, scroll it so it's visible
// Your app might not need or want this behavior.
CGRect aRect = self.view.frame;
aRect.size.height -= kbSize.height;
if (!CGRectContainsPoint(aRect, self.textView.frame.origin) ) {
    [self.textView scrollRectToVisible:self.textView.frame animated:YES];
}
}

// Called when the UIKeyboardWillHideNotification is sent
- (void)keyboardWasHidden:(NSNotification*)aNotification
{
    UIEdgeInsets contentInsets = UIEdgeInsetsZero;
    self.textView.contentInset = contentInsets;
    self.textView.scrollIndicatorInsets = contentInsets;
}
Hemant Singh Rathore
  • 2,153
  • 1
  • 24
  • 38
1

if you have more then 1 textfield or you want to reduce your code then try this code

- (void)textFieldDidBeginEditing:(UITextField *)textField{

    [UIView beginAnimations:nil context:NULL];
    [UIView setAnimationDuration:0.35f];
    CGRect frame = self.view.frame;
    frame.origin.y = (self.view.frame.size.height - textField.frame.origin.y) - self.view.frame.size.height+60;
    if (frame.origin.y<-162) {
        frame.origin.y = -162;
    }
    [self.view setFrame:frame];
    [UIView commitAnimations];
}
-(BOOL)textFieldShouldEndEditing:(UITextField *)textField{
    [UIView beginAnimations:nil context:NULL];
    [UIView setAnimationDuration:0.35f];
    CGRect frame = self.view.frame;
    frame.origin.y = 0;
    [self.view setFrame:frame];
    [UIView commitAnimations];
    return YES;

}
Milan Vadgama
  • 321
  • 3
  • 7
0

Extending @alejandro & @Mani :

Th final answer:

- (void)keyboardWasShown:(NSNotification *)notification {
    if (self.textView != nil) {
        NSDictionary* info = [notification userInfo];
        CGRect keyboardRect = [self.textNote convertRect:[[info objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue] fromView:nil];
        CGSize keyboardSize = keyboardRect.size;

        self.textView.contentInset = UIEdgeInsetsMake(0, 0, keyboardSize.height, 0);
        self.textView.scrollIndicatorInsets = self.textView.contentInset;
    }
}

- (void)keyboardWillBeHidden:(NSNotification*)notification {

     self.textView.scrollIndicatorInsets = UIEdgeInsetsZero;
    self.textView.scrollIndicatorInsets = UIEdgeInsetsZero;
}
Yogesh Lolusare
  • 2,162
  • 1
  • 24
  • 35