51

I have 6 UITextFields on my UIScrollView. Now, I can scroll by user request. But when the keyboard appear, some textfields are hidden.

That is not user-friendly.

How scroll programmatically the view so I get sure the keyboard not hide the textfield?

Cœur
  • 37,241
  • 25
  • 195
  • 267
mamcx
  • 15,916
  • 26
  • 101
  • 189
  • see the answer I posted for http://stackoverflow.com/questions/484839/how-get-the-list-of-controls-on-a-view-with-iphone-sdk – Ryan Townshend Jan 27 '09 at 19:37

12 Answers12

79

Here's what worked for me. Having an instance variable that holds the value of the UIScrollView's offset before the view is adjusted for the keyboard so you can restore the previous state after the UITextField returns:

//header
@interface TheViewController : UIViewController <UITextFieldDelegate> {
    CGPoint svos;
}


//implementation
- (void)textFieldDidBeginEditing:(UITextField *)textField {
    svos = scrollView.contentOffset;
    CGPoint pt;
    CGRect rc = [textField bounds];
    rc = [textField convertRect:rc toView:scrollView];
    pt = rc.origin;
    pt.x = 0;
    pt.y -= 60;
    [scrollView setContentOffset:pt animated:YES];           
}

- (BOOL)textFieldShouldReturn:(UITextField *)textField {
    [scrollView setContentOffset:svos animated:YES]; 
    [textField resignFirstResponder];
    return YES;
}
james_womack
  • 10,028
  • 6
  • 55
  • 74
  • 2
    Just a minor addition: Put svos = scrollView.contentOffset; in the ViewDidLoad method if you have multiple UITextFields. Because if you go from one text field to the next without hiding the keyboard svos will be overwritten and therefore restoring the content offset will fail. – Volker Voecking Jul 20 '10 at 10:10
  • In my case I did not want to restore the offset present when the view first loads, but just before each individual UITextField is accessed. – james_womack Jul 20 '10 at 16:00
  • Can some body Explain me about CGPoint pt; CGRect rc = [textField bounds]; rc = [textField convertRect:rc toView:scrollView]; pt = rc.origin; pt.x = 0; pt.y -= 60; what does this mean.What I am looking is to have many TextFields in ScrollView – Ajay Sharma Mar 18 '11 at 10:54
  • this code doesn't work for me as of xcode 4.3.1 - textFieldDidBeginEditing receives a NSNotification object, not a UITextField... Unless I'm missing something. I was able to work around that, but not in a way that's pertinent to this question. – Dakine83 Mar 13 '12 at 04:45
  • @Dakine83 You are missing something: - (void)textFieldDidBeginEditing:(UITextField *)textField is in the documentation. Your version of Xcode doesn't matter either, it's the version of iOS that matters. – james_womack Mar 14 '12 at 07:48
37

Finally, a simple fix:

UIScrollView* v = (UIScrollView*) self.view ;
CGRect rc = [textField bounds];
rc = [textField convertRect:rc toView:v];
rc.origin.x = 0 ;
rc.origin.y -= 60 ;

rc.size.height = 400;
[self.scroll scrollRectToVisible:rc animated:YES];

Now I think is only combine this with the link above and is set!

itsji10dra
  • 4,603
  • 3
  • 39
  • 59
mamcx
  • 15,916
  • 26
  • 101
  • 189
  • Works in iOS8. I don't know about TextFields at the bottom but a workaround could be extendeing the scrollable content down by the height of the virtual keyboard. You would have empty space at the bottom, but that space would be occupied by the virtual keyboard when editing. – Lensflare Oct 21 '14 at 13:46
21

I've put together a universal, drop-in UIScrollView and UITableView subclass that takes care of moving all text fields within it out of the way of the keyboard.

When the keyboard is about to appear, the subclass will find the subview that's about to be edited, and adjust its frame and content offset to make sure that view is visible, with an animation to match the keyboard pop-up. When the keyboard disappears, it restores its prior size.

It should work with basically any setup, either a UITableView-based interface, or one consisting of views placed manually.

Here it is.


(For google: TPKeyboardAvoiding, TPKeyboardAvoidingScrollView, TPKeyboardAvoidingCollectionView.)
Editor's note: TPKeyboardAvoiding seems to be continually updated and fresh, as of 2014.
itsji10dra
  • 4,603
  • 3
  • 39
  • 59
Michael Tyson
  • 1,498
  • 1
  • 14
  • 23
  • I've been trying to work around the bugs in this thing. First, no scrolling would occur until I found a hint about setting self.myScrollView.contentInset = UIEdgeInsetsMake(0, 0, self.view.bounds.size.height + 432, 0); // (top,left,bottom,right) – Bruce Patin Sep 09 '15 at 15:28
  • Now, even if the field is scrolled above where the keyboard would be before I focus on it, the scroll is undone and the keyboard covers it. This is very frustrating. I am going to do it the down and dirty way, using http://www.codeproject.com/Articles/744418/Virtual-Keyboard-in-iOS-Part and the other answers on this page. – Bruce Patin Sep 09 '15 at 15:31
2

simple and best

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

  // self.scrlViewUI.contentOffset = CGPointMake(0, textField.frame.origin.y);
    [_scrlViewUI setContentOffset:CGPointMake(0,textField.center.y-90) animated:YES];
    tes=YES;
    [self viewDidLayoutSubviews];
}
Stewie Griffin
  • 14,889
  • 11
  • 39
  • 70
NRV
  • 21
  • 1
2

If you set the delegate of your text fields to a controller object in your program, you can have that object implement the textFieldDidBeginEditing: and textFieldShouldReturn: methods. The first method can then be used to scroll to your text field and the second method can be used to scroll back.

You can find code I have used for this in my blog: Sliding UITextViews around to avoid the keyboard. I didn't test this code for text views in a UIScrollView but it should work.

Matt Gallagher
  • 14,858
  • 2
  • 41
  • 43
1

The answers posted so far didn't work for me as I've a quite deep nested structure of UIViews. Also, the I had the problem that some of those answers were working only on certain device orientations.

Here's my solution, which will hopefully make you waste some less time on this.

My UIViewTextView derives from UIView, is a UITextView delegate and adds a UITextView after having read some parameters from an XML file for that UITextView (that XML part is left out here for clarity).

Here's the private interface definition:

#import "UIViewTextView.h"
#import <CoreGraphics/CoreGraphics.h>
#import <CoreGraphics/CGColor.h>

@interface UIViewTextView (/**/) {
  @private
  UITextView *tf;

  /*
   * Current content scroll view
   * position and frame
   */
  CGFloat currentScrollViewPosition;
  CGFloat currentScrollViewHeight;
  CGFloat kbHeight;
  CGFloat kbTop;

  /*
   * contentScrollView is the UIScrollView
   * that contains ourselves.
   */
  UIScrollView contentScrollView;
}
@end

In the init method I have to register the event handlers:

@implementation UIViewTextView

- (id) initWithScrollView:(UIScrollView*)scrollView {
  self              = [super init];

  if (self) {
    contentScrollView = scrollView;

    // ...

    tf = [[UITextView alloc] initWithFrame:CGRectMake(0, 0, 241, 31)];

    // ... configure tf and fetch data for it ...

    tf.delegate = self;

    // ...

    NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
    [nc addObserver:self selector:@selector(keyboardWasShown:) name: UIKeyboardWillShowNotification object:nil];
    [nc addObserver:self selector:@selector(keyboardWasHidden:) name: UIKeyboardWillHideNotification object:nil];
    [self addSubview:tf];
  }

  return(self);
}

Once that's done, we need to handle the keyboard show event. This gets called before the textViewBeginEditing is called, so we can use it to find out some properties of the keyboard. In essence, we want to know the height of the keyboard. This, unfortunately, needs to be taken from its width property in landscape mode:

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

  CGRect screenRect                  = [[UIScreen mainScreen] bounds];
  CGFloat sWidth                     = screenRect.size.width;
  CGFloat sHeight                    = screenRect.size.height;

  UIInterfaceOrientation orientation = [[UIApplication sharedApplication] statusBarOrientation];

  if ((orientation == UIDeviceOrientationPortrait)
      ||(orientation == UIDeviceOrientationPortraitUpsideDown)) {
    kbHeight     = kbSize.height;
    kbTop        = sHeight - kbHeight;
  } else {
    //Note that the keyboard size is not oriented
    //so use width property instead
    kbHeight     = kbSize.width;
    kbTop        = sWidth - kbHeight;
  }

Next, we need to actually scroll around when we start editing. We do this here:

- (void) textViewDidBeginEditing:(UITextView *)textView {
  /*
   * Memorize the current scroll position
   */
  currentScrollViewPosition = contentScrollView.contentOffset.y;

  /*
   * Memorize the current scroll view height
   */
  currentScrollViewHeight   = contentScrollView.frame.size.height;

  // My top position
  CGFloat myTop    = [self convertPoint:self.bounds.origin toView:[UIApplication sharedApplication].keyWindow.rootViewController.view].y;

  // My height
  CGFloat myHeight = self.frame.size.height;

  // My bottom
  CGFloat myBottom = myTop + myHeight;

  // Eventual overlap
  CGFloat overlap  = myBottom - kbTop;

  /*
   * If there's no overlap, there's nothing to do.
   */
  if (overlap < 0) {
    return;
  }

  /*
   * Calculate the new height
   */
  CGRect crect = contentScrollView.frame;
  CGRect nrect = CGRectMake(crect.origin.x, crect.origin.y, crect.size.width, currentScrollViewHeight + overlap);

  /*
   * Set the new height
   */
  [contentScrollView setFrame:nrect];

  /*
   * Set the new scroll position
   */
  CGPoint npos;

  npos.x = contentScrollView.contentOffset.x;
  npos.y = contentScrollView.contentOffset.y + overlap;

  [contentScrollView setContentOffset:npos animated:NO];
}

When we end editing, we do this to reset the scroll position:

- (void) textViewDidEndEditing:(UITextView *)textView {
  /*
   * Reset the scroll view position
   */
  CGRect crect = contentScrollView.frame;
  CGRect nrect = CGRectMake(crect.origin.x, crect.origin.y, crect.size.width, currentScrollViewHeight);

  [contentScrollView setFrame:nrect];

  /*
   * Reset the scroll view height
   */
  CGPoint npos;

  npos.x = contentScrollView.contentOffset.x;
  npos.y = currentScrollViewPosition;

  [contentScrollView setContentOffset:npos animated:YES];
  [tf resignFirstResponder];

  // ... do something with your data ...

}

There's nothing left to do in the keyboard was hidden event handler; we leave it in anyway:

-(void)keyboardWasHidden:(NSNotification*)aNotification {
}

And that's it.

/*
   // Only override drawRect: if you perform custom drawing.
   // An empty implementation adversely affects performance during animation.
   - (void)drawRect:(CGRect)rect
   {
   // Drawing code
   }
 */

@end
Matthias Nott
  • 301
  • 2
  • 4
1

I know this is old, but still none of the solutions above had all the fancy positioning stuff required for that "perfect" bug-free, backwards compatible and flicker-free animation.
Let me share my solution (assuming you have set up UIKeyboardWill(Show|Hide)Notification):

// Called when UIKeyboardWillShowNotification is sent
- (void)keyboardWillShow:(NSNotification*)notification
{
    // if we have no view or are not visible in any window, we don't care
    if (!self.isViewLoaded || !self.view.window) {
        return;
    }

    NSDictionary *userInfo = [notification userInfo];

    CGRect keyboardFrameInWindow;
    [[userInfo objectForKey:UIKeyboardFrameEndUserInfoKey] getValue:&keyboardFrameInWindow];

    // the keyboard frame is specified in window-level coordinates. this calculates the frame as if it were a subview of our view, making it a sibling of the scroll view
    CGRect keyboardFrameInView = [self.view convertRect:keyboardFrameInWindow fromView:nil];

    CGRect scrollViewKeyboardIntersection = CGRectIntersection(_scrollView.frame, keyboardFrameInView);
    UIEdgeInsets newContentInsets = UIEdgeInsetsMake(0, 0, scrollViewKeyboardIntersection.size.height, 0);

    // this is an old animation method, but the only one that retains compaitiblity between parameters (duration, curve) and the values contained in the userInfo-Dictionary.
    [UIView beginAnimations:nil context:NULL];
    [UIView setAnimationDuration:[[userInfo objectForKey:UIKeyboardAnimationDurationUserInfoKey] doubleValue]];
    [UIView setAnimationCurve:[[userInfo objectForKey:UIKeyboardAnimationCurveUserInfoKey] intValue]];

    _scrollView.contentInset = newContentInsets;
    _scrollView.scrollIndicatorInsets = newContentInsets;

    /*
     * Depending on visual layout, _focusedControl should either be the input field (UITextField,..) or another element
     * that should be visible, e.g. a purchase button below an amount text field
     * it makes sense to set _focusedControl in delegates like -textFieldShouldBeginEditing: if you have multiple input fields
     */
    if (_focusedControl) {
        CGRect controlFrameInScrollView = [_scrollView convertRect:_focusedControl.bounds fromView:_focusedControl]; // if the control is a deep in the hierarchy below the scroll view, this will calculate the frame as if it were a direct subview
        controlFrameInScrollView = CGRectInset(controlFrameInScrollView, 0, -10); // replace 10 with any nice visual offset between control and keyboard or control and top of the scroll view.

        CGFloat controlVisualOffsetToTopOfScrollview = controlFrameInScrollView.origin.y - _scrollView.contentOffset.y;
        CGFloat controlVisualBottom = controlVisualOffsetToTopOfScrollview + controlFrameInScrollView.size.height;

        // this is the visible part of the scroll view that is not hidden by the keyboard
        CGFloat scrollViewVisibleHeight = _scrollView.frame.size.height - scrollViewKeyboardIntersection.size.height;

        if (controlVisualBottom > scrollViewVisibleHeight) { // check if the keyboard will hide the control in question
            // scroll up until the control is in place
            CGPoint newContentOffset = _scrollView.contentOffset;
            newContentOffset.y += (controlVisualBottom - scrollViewVisibleHeight);

            // make sure we don't set an impossible offset caused by the "nice visual offset"
            // if a control is at the bottom of the scroll view, it will end up just above the keyboard to eliminate scrolling inconsistencies
            newContentOffset.y = MIN(newContentOffset.y, _scrollView.contentSize.height - scrollViewVisibleHeight);

            [_scrollView setContentOffset:newContentOffset animated:NO]; // animated:NO because we have created our own animation context around this code
        } else if (controlFrameInScrollView.origin.y < _scrollView.contentOffset.y) {
            // if the control is not fully visible, make it so (useful if the user taps on a partially visible input field
            CGPoint newContentOffset = _scrollView.contentOffset;
            newContentOffset.y = controlFrameInScrollView.origin.y;

            [_scrollView setContentOffset:newContentOffset animated:NO]; // animated:NO because we have created our own animation context around this code
        }
    }

    [UIView commitAnimations];
}


// Called when the UIKeyboardWillHideNotification is sent
- (void)keyboardWillHide:(NSNotification*)notification
{
    // if we have no view or are not visible in any window, we don't care
    if (!self.isViewLoaded || !self.view.window) {
        return;
    }

    NSDictionary *userInfo = notification.userInfo;

    [UIView beginAnimations:nil context:NULL];
    [UIView setAnimationDuration:[[userInfo valueForKey:UIKeyboardAnimationDurationUserInfoKey] doubleValue]];
    [UIView setAnimationCurve:[[userInfo valueForKey:UIKeyboardAnimationCurveUserInfoKey] intValue]];

    // undo all that keyboardWillShow-magic
    // the scroll view will adjust its contentOffset apropriately
    _scrollView.contentInset = UIEdgeInsetsZero;
    _scrollView.scrollIndicatorInsets = UIEdgeInsetsZero;

    [UIView commitAnimations];
}
Martin Ullrich
  • 94,744
  • 25
  • 252
  • 217
1

You may check it out: https://github.com/michaeltyson/TPKeyboardAvoiding (I used that sample for my apps). It is working so well. I hope that helps you.


Actually, here's a full tutorial on using TPKeyboardAvoiding, which may help someone

(1) download the zip file from the github link. add these four files to your Xcode project:

enter image description here

(2) build your beautiful form in IB. add a UIScrollView. sit the form items INSIDE the scroll view. (Note - extremely useful tip regarding interface builder: https://stackoverflow.com/a/16952902/294884)

enter image description here

(3) click on the scroll view. then at the top right, third button, you'll see the word "UIScrollView". using copy and paste, change it to "TPKeyboardAvoidingScrollView"

enter image description here

(4) that's it. put the app in the app store, and bill your client.

(Also, just click on the Inspector tab of the scroll view. You may prefer to turn on or off bouncing and the scroll bars - your preference.)

Personal comment - I strongly recommend using scroll view (or collection view) for input forms, in almost all cases. do not use a table view. it's problematic for many reasons. and quite simply, it's incredibly easier to use a scroll view. just lay it out any way you want. it is 100% wysiwyg in interface builder. hope it helps

Community
  • 1
  • 1
hightech
  • 3,742
  • 3
  • 25
  • 36
  • Sure, that's michael tyson's class - just scroll down to his answer below! it's amazing http://stackoverflow.com/a/5634493/294884 – Fattie Mar 31 '14 at 17:18
  • @JoeBlow oh, I didn't see that. I'm glad he answered it. I gave him +1 vote. – hightech Mar 31 '14 at 18:33
  • Right - I gave MT **fifty points**. That class is so absolutely amazing, and free, that everyone should click over a bounty on it. There's literally just no other solution. It invokes **absolutely no - zero - work**. Well, I guess you have to paste in the word "TPKeyboardAvoidingScrollView" in interface builder. It's incredible anyone bothers doing anything else. It literally saves my clients $1000s on every project. There's just no other solution worth looking at. – Fattie Apr 01 '14 at 10:58
1

This is my code, hope it will help you. It work ok in case you have many textfield

CGPoint contentOffset;
bool isScroll;
- (void)textFieldDidBeginEditing:(UITextField *)textField {
    contentOffset = self.myScroll.contentOffset;
    CGPoint newOffset;
    newOffset.x = contentOffset.x;
    newOffset.y = contentOffset.y;
    //check push return in keyboar
    if(!isScroll){
        //180 is height of keyboar
        newOffset.y += 180;
        isScroll=YES;
    }
   [self.myScroll setContentOffset:newOffset animated:YES];
}

- (BOOL)textFieldShouldReturn:(UITextField *)textField{
    //reset offset of content
    isScroll = NO;
    [self.myScroll setContentOffset:contentOffset animated:YES];
    [textField endEditing:true];
    return  true;
}

we have a point contentOffset to save contentoffset of scrollview before keyboar show. Then we will scroll content for y about 180 (height of keyboar). when you touch return in keyboar, we will scroll content to old point(it is contentOffset). If you have many textfield, you don't touch return in keyboar but you touch another textfield, it will +180 . So we have check touch return

Heo Đất Hades
  • 1,573
  • 18
  • 14
  • we have a point contentOffset to save contentoffset of scrollview before keyboar show. Then we will scroll content for y about 180 (height of keyboar). when you touch return in keyboar, we will scroll content to old point(it is contentOffset). If you have many textfield, you don't touch return in keyboar but you touch another textfield, it will +180 . So we have check touch return – Heo Đất Hades Jan 29 '15 at 10:10
  • 1
    Please provide some explanation or this answer will get deleted. – Paco Jan 29 '15 at 15:18
0

Use any of these,

CGPoint bottomOffset = CGPointMake(0, self.MainScrollView.contentSize.height - self.MainScrollView.bounds.size.height);

[self.MainScrollView setContentOffset:bottomOffset animated:YES];

or

[self.MainScrollView scrollRectToVisible:CGRectMake(0, self.MainScrollView.contentSize.height - self.MainScrollView.bounds.size.height-30, MainScrollView.frame.size.width, MainScrollView.frame.size.height) animated:YES];
Ramdhas
  • 1,765
  • 1
  • 18
  • 26
0

I think it's better use keyboard notifications because you don't know if the first responder (the control with focus on) is a textField or a textView (or whatever). So juste create a category to find the first responder :

#import "UIResponder+FirstResponder.h"

static __weak id currentFirstResponder;

@implementation UIResponder (FirstResponder)

+(id)currentFirstResponder {
   currentFirstResponder = nil;
   [[UIApplication sharedApplication] sendAction:@selector(findFirstResponder:) to:nil from:nil forEvent:nil];
   return currentFirstResponder;
}

-(void)findFirstResponder:(id)sender {
   currentFirstResponder = self;
}

@end

then

-(void)keyboardWillShowNotification:(NSNotification*)aNotification{

    contentScrollView.delegate=nil;
    contentScrollView.scrollEnabled=NO;
    contentScrollViewOriginalOffset = contentScrollView.contentOffset;

    UIResponder *lc_firstResponder = [UIResponder currentFirstResponder];
    if([lc_firstResponder isKindOfClass:[UIView class]]){

        UIView *lc_view = (UIView *)lc_firstResponder;

        CGRect lc_frame = [lc_view convertRect:lc_view.bounds toView:contentScrollView];
        CGPoint lc_point = CGPointMake(0, lc_frame.origin.y-lc_frame.size.height);
        [contentScrollView setContentOffset:lc_point animated:YES];
    }
}

Eventually disable the scroll and set the delegate to nil then restore it to avoid some actions during the edition of the first responder. Like james_womack said, keep the original offset to restore it in a keyboardWillHideNotification method.

-(void)keyboardWillHideNotification:(NSNotification*)aNotification{

    contentScrollView.delegate=self;
    contentScrollView.scrollEnabled=YES;
    [contentScrollView setContentOffset:contentScrollViewOriginalOffset animated:YES];
}
Bejil
  • 412
  • 5
  • 18
  • Don't know why but in some cases first responder will be nil if the keyboard notification is "will" type. If you set your notifications with UIKeyboardDidShowNotification and UIKeyboardDidHideNotification, you avoid chances to get a nil responder. – Bejil Sep 02 '15 at 12:11
-1

In Swift 1.2+ do something like this:

class YourViewController: UIViewController, UITextFieldDelegate {

    override func viewDidLoad() {
        super.viewDidLoad()

        _yourTextField.delegate = self //make sure you have the delegate set to this view controller for each of your textFields so textFieldDidBeginEditing can be called for each one
        ...

    }

    func textFieldDidBeginEditing(textField: UITextField) {
        var point = textField.convertPoint(textField.frame.origin, toView: _yourScrollView)
        point.x = 0.0 //if your textField does not have an origin at 0 for x and you don't want your scrollView to shift left and right but rather just up and down
        _yourScrollView.setContentOffset(point, animated: true)
    }

    func textFieldDidEndEditing(textField: UITextField) {
        //Reset scrollview once done editing
        scrollView.setContentOffset(CGPoint.zero, animated: true)
    }

}
Chris Klingler
  • 5,258
  • 2
  • 37
  • 43