37

I've seen posts around here that suggest that UIScrollViews should automatically scroll if a subview UITextField becomes the first responder; however, I can't figure out how to get this to work.

What I have is a UIViewController that has a UIScrollView and within the UIScrollView there are multiple textfields.

I know how to do this manually if necessary; however, from what I've been reading, it seems possible to have it autoscroll. Help please.

Community
  • 1
  • 1
Nosrettap
  • 10,216
  • 22
  • 77
  • 133
  • 1
    I believe the autoscrolling they are talking about is the default behavior. If, for example, you select a textfield near the bottom of the view where the keyboard would be(but is not yet), then the textfield gets automatically scrolled so it is visible just above the keyboard when the keyboard slides up. I believe this would also happen if you programmatically selected a textfield that was not currently on the screen with something like [textview1 becomeFirstResponder]; – Ryan Oct 24 '12 at 20:15

11 Answers11

43

I hope this example will help you You can scroll to any point by this code.

scrollView.contentOffset = CGPointMake(0,0);

So if you have textfield, it must have some x,y position on view, so you can use

CGPoint point = textfield.frame.origin ;
scrollView.contentOffset = point 

This should do the trick,

But if you don't know when to call this code, so you should learn UITextFieldDelegate methods

Implement this method in your code

- (void)textFieldDidBeginEditing:(UITextField *)textField {
// Place Scroll Code here
}

I hope you know how to use delegate methods.

Adeel Pervaiz
  • 1,256
  • 10
  • 10
  • 7
    The question is how to make auto scroll, but not how to implement it manually. – onegray Oct 24 '12 at 21:37
  • 11
    I would use [self.scrollView setContentOffset:point animated:YES] to make the transition nice and smooth :) – ChrisBorg Nov 14 '13 at 18:18
  • @ChrisBorg iOS 7 and upwards don't seem to actually scroll when doing that. So that doesn't really achieve the desired effect. – Supertecnoboff Oct 21 '15 at 09:40
  • Do ```[scroll setContentOffset:CGPointMake(0, point.y) animated:YES];``` to get the scroll view to animate the offset. – Supertecnoboff Oct 21 '15 at 09:46
  • @AdeelPervaiz There is still one problem though. What if you have set the scroll view to only scroll a short distance? Then this won't work because it can't scroll right? For example ```[scroll setContentSize:CGSizeMake(320, 690)];``` Well if ```point.y``` is greater than 690, then it won't work??? – Supertecnoboff Oct 21 '15 at 09:48
  • @Supertecnoboff What are you trying to reproduce, A glitch in IOS ? – Adeel Pervaiz Oct 31 '15 at 20:05
  • This is so much easier than the convoluted bs I was doing before. – Rob Oct 16 '18 at 19:28
14

I know this question has already been answered, but I thought I would share the code combination that I used from @Adeel and @Basil answer, as it seems to work perfectly for me on iOS 9.

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

    // Scroll to the text field so that it is
    // not hidden by the keyboard during editing.
    [scroll setContentOffset:CGPointMake(0, (textField.superview.frame.origin.y + (textField.frame.origin.y))) animated:YES];
}

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

    // Remove any content offset from the scroll
    // view otherwise the scroll view will look odd.
    [scroll setContentOffset:CGPointMake(0, 0) animated:YES];
}

I also used the animated method, it makes for a much smoother transition.

Supertecnoboff
  • 5,886
  • 9
  • 50
  • 94
  • 2
    Works perfectly for me. Thanks! I wasted a ton of time trying to get a solution working based on the keyBoard appearing, and then searching for which UITextView triggered it. This solution avoids that search. – Scooter Feb 09 '16 at 17:09
  • I found that with this solution, if the `UITextField` is in a scroll view and you scroll, it will reset the contentOffset to the default, basically hiding the text field back behind the keyboard. – Jonathan Cabrera Sep 06 '18 at 21:08
8

There is nothing you have to do manually. It is the default behavior. There are two possibilities as to why you are not seeing the behavior

  1. The most likely reason is that the keyboard is covering your UITextField. See below for solution
  2. The other possibility is that you have another UIScrollView somewhere in the view hierarchy between the UITextField and the UIScrollView that you want to auto scroll. This is less likely but can still cause problems.

For #1, you want to implement something similar to Apple's recommendations for Moving Content That Is Located Under the Keyboard. Note that the code provided by Apple does not account for rotation. For improvements on their code, check out this blog post's implementation of the keyboardDidShow method that properly translates the keyboard's frame using the window.

Michael McGuire
  • 3,473
  • 1
  • 26
  • 27
  • This could also be happening if the constraints determining the scroll view's content size are ambiguous. Since my scroll view scrolls only vertically, the only constraints I set up for the scroll view's `contentLayoutGuide` were for the `topAnchor` and `bottomAnchor`. Adding constraints for the leading and trailing anchors made the content size fully non-ambiguous and solved the problem for me. – jsadler Jan 06 '21 at 16:16
8

Here is the Swift 4 update to @Supertecnoboff's answer. It worked great for me.

func textFieldDidBeginEditing(_ textField: UITextField) {
    scroll.setContentOffset(CGPoint(x: 0, y: (textField.superview?.frame.origin.y)!), animated: true)
}

func textFieldDidEndEditing(_ textField: UITextField) {
    scroll.setContentOffset(CGPoint(x: 0, y: 0), animated: true)
}

Make sure to extend UITextFieldDelegate and set the textfields' delegate to self.

donmiller
  • 91
  • 1
  • 4
5
- (void)textFieldDidBeginEditing:(UITextField *)textField {
    CGRect rect = [textField bounds];
    rect = [textField convertRect:rect toView:self.scrollView];
    rect.origin.x = 0 ;
    rect.origin.y -= 60 ;
    rect.size.height = 400;

    [self.scrollView scrollRectToVisible:rect animated:YES];
}
George
  • 746
  • 1
  • 9
  • 16
3

You can use this function for autoScroll of UITextField

on UITextFieldDelegate

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

[self autoScrolTextField:textField onScrollView:self.scrollView];
}




- (void) autoScrolTextField: (UITextField *) textField onScrollView: (UIScrollView *) scrollView { 
 float slidePoint = 0.0f;
float keyBoard_Y_Origin = self.view.bounds.size.height - 216.0f;
float textFieldButtomPoint = textField.superview.frame.origin.y + (textField.frame.origin.y + textField.frame.size.height);

if (keyBoard_Y_Origin < textFieldButtomPoint - scrollView.contentOffset.y) {
    slidePoint = textFieldButtomPoint - keyBoard_Y_Origin + 10.0f;
    CGPoint point = CGPointMake(0.0f, slidePoint);
    scrollView.contentOffset = point;
}

EDIT:

Im now using IQKeyboardManager Kudos to the developer of this, you need to try this.

2

Solution

extension UIScrollView {
    func scrollVerticallyToFirstResponderSubview(keyboardFrameHight: CGFloat) {
        guard let firstResponderSubview = findFirstResponderSubview() else { return }
        scrollVertically(toFirstResponder: firstResponderSubview,
                         keyboardFrameHight: keyboardFrameHight, animated: true)
    }

    private func scrollVertically(toFirstResponder view: UIView,
                                  keyboardFrameHight: CGFloat, animated: Bool) {
        let scrollViewVisibleRectHeight = frame.height - keyboardFrameHight
        let maxY = contentSize.height - scrollViewVisibleRectHeight
        if contentOffset.y >= maxY { return }
        var point = view.convert(view.bounds.origin, to: self)
        point.x = 0
        point.y -= scrollViewVisibleRectHeight/2
        if point.y > maxY {
            point.y = maxY
        } else if point.y < 0 {
            point.y = 0
        }
        setContentOffset(point, animated: true)
    }
}

extension UIView {
    func findFirstResponderSubview() -> UIView? { getAllSubviews().first { $0.isFirstResponder } }
    func getAllSubviews<T: UIView>() -> [T] { UIView.getAllSubviews(from: self) as [T] }
    class func getAllSubviews<T: UIView>(from parenView: UIView) -> [T] {
        parenView.subviews.flatMap { subView -> [T] in
            var result = getAllSubviews(from: subView) as [T]
            if let view = subView as? T { result.append(view) }
            return result
        }
    }
}

Full Sample

Do not forget to paste the Solution code here

import UIKit

class ViewController: UIViewController {

    private weak var scrollView: UIScrollView!
    private lazy var keyboard = KeyboardNotifications(notifications: [.willHide, .willShow], delegate: self)
    
    override func viewDidLoad() {
        super.viewDidLoad()
        let scrollView = UIScrollView()
        view.addSubview(scrollView)
        scrollView.translatesAutoresizingMaskIntoConstraints = false
        scrollView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor).isActive = true
        scrollView.leftAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leftAnchor).isActive = true
        scrollView.rightAnchor.constraint(equalTo: view.safeAreaLayoutGuide.rightAnchor).isActive = true
        scrollView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor).isActive = true
        scrollView.contentSize = CGSize(width: view.frame.width, height: 1000)
        scrollView.isScrollEnabled = true
        scrollView.indicatorStyle = .default
        scrollView.backgroundColor = .yellow
        scrollView.keyboardDismissMode = .interactive
        self.scrollView = scrollView
        
        addTextField(y: 20)
        addTextField(y: 300)
        addTextField(y: 600)
        addTextField(y: 950)
    }
    
    private func addTextField(y: CGFloat) {
        let textField = UITextField()
        textField.borderStyle = .line
        scrollView.addSubview(textField)
        textField.translatesAutoresizingMaskIntoConstraints = false
        textField.topAnchor.constraint(equalTo: scrollView.topAnchor, constant: y).isActive = true
        textField.leftAnchor.constraint(equalTo: scrollView.leftAnchor, constant: 44).isActive = true
        textField.widthAnchor.constraint(equalToConstant: 120).isActive = true
        textField.heightAnchor.constraint(equalToConstant: 44).isActive = true
    }
    
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        keyboard.isEnabled = true
    }
    
    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        keyboard.isEnabled = false
    }
}

extension ViewController: KeyboardNotificationsDelegate {
    func keyboardWillShow(notification: NSNotification) {
        guard   let userInfo = notification.userInfo as? [String: Any],
                let keyboardFrame = userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect else { return }
        scrollView.contentInset.bottom = keyboardFrame.height
        scrollView.scrollVerticallyToFirstResponderSubview(keyboardFrameHight: keyboardFrame.height)
    }

    func keyboardWillHide(notification: NSNotification) {
        scrollView.contentInset.bottom = 0
    }
}

/// Solution

extension UIScrollView {
    func scrollVerticallyToFirstResponderSubview(keyboardFrameHight: CGFloat) {
        guard let firstResponderSubview = findFirstResponderSubview() else { return }
        scrollVertically(toFirstResponder: firstResponderSubview,
                         keyboardFrameHight: keyboardFrameHight, animated: true)
    }

    private func scrollVertically(toFirstResponder view: UIView,
                                  keyboardFrameHight: CGFloat, animated: Bool) {
        let scrollViewVisibleRectHeight = frame.height - keyboardFrameHight
        let maxY = contentSize.height - scrollViewVisibleRectHeight
        if contentOffset.y >= maxY { return }
        var point = view.convert(view.bounds.origin, to: self)
        point.x = 0
        point.y -= scrollViewVisibleRectHeight/2
        if point.y > maxY {
            point.y = maxY
        } else if point.y < 0 {
            point.y = 0
        }
        setContentOffset(point, animated: true)
    }
}

extension UIView {
    func findFirstResponderSubview() -> UIView? { getAllSubviews().first { $0.isFirstResponder } }
    func getAllSubviews<T: UIView>() -> [T] { UIView.getAllSubviews(from: self) as [T] }
    class func getAllSubviews<T: UIView>(from parenView: UIView) -> [T] {
        parenView.subviews.flatMap { subView -> [T] in
            var result = getAllSubviews(from: subView) as [T]
            if let view = subView as? T { result.append(view) }
            return result
        }
    }
}

// https://stackoverflow.com/a/42600092/4488252

import Foundation

protocol KeyboardNotificationsDelegate: class {
    func keyboardWillShow(notification: NSNotification)
    func keyboardWillHide(notification: NSNotification)
    func keyboardDidShow(notification: NSNotification)
    func keyboardDidHide(notification: NSNotification)
}

extension KeyboardNotificationsDelegate {
    func keyboardWillShow(notification: NSNotification) {}
    func keyboardWillHide(notification: NSNotification) {}
    func keyboardDidShow(notification: NSNotification) {}
    func keyboardDidHide(notification: NSNotification) {}
}

class KeyboardNotifications {

    fileprivate var _isEnabled: Bool
    fileprivate var notifications: [NotificationType]
    fileprivate weak var delegate: KeyboardNotificationsDelegate?
    fileprivate(set) lazy var isKeyboardShown: Bool = false

    init(notifications: [NotificationType], delegate: KeyboardNotificationsDelegate) {
        _isEnabled = false
        self.notifications = notifications
        self.delegate = delegate
    }

    deinit { if isEnabled { isEnabled = false } }
}

// MARK: - enums

extension KeyboardNotifications {

    enum NotificationType {
        case willShow, willHide, didShow, didHide

        var selector: Selector {
            switch self {
                case .willShow: return #selector(keyboardWillShow(notification:))
                case .willHide: return #selector(keyboardWillHide(notification:))
                case .didShow: return #selector(keyboardDidShow(notification:))
                case .didHide: return #selector(keyboardDidHide(notification:))
            }
        }

        var notificationName: NSNotification.Name {
            switch self {
                case .willShow: return UIResponder.keyboardWillShowNotification
                case .willHide: return UIResponder.keyboardWillHideNotification
                case .didShow: return UIResponder.keyboardDidShowNotification
                case .didHide: return UIResponder.keyboardDidHideNotification
            }
        }
    }
}

// MARK: - isEnabled

extension KeyboardNotifications {

    private func addObserver(type: NotificationType) {
        NotificationCenter.default.addObserver(self, selector: type.selector, name: type.notificationName, object: nil)
    }

    var isEnabled: Bool {
        set {
            if newValue {
                for notificaton in notifications { addObserver(type: notificaton) }
            } else {
                NotificationCenter.default.removeObserver(self)
            }
            _isEnabled = newValue
        }

        get { _isEnabled }
    }

}

// MARK: - Notification functions

extension KeyboardNotifications {

    @objc func keyboardWillShow(notification: NSNotification) {
        delegate?.keyboardWillShow(notification: notification)
        isKeyboardShown = true
    }

    @objc func keyboardWillHide(notification: NSNotification) {
        delegate?.keyboardWillHide(notification: notification)
        isKeyboardShown = false
    }

    @objc func keyboardDidShow(notification: NSNotification) {
        isKeyboardShown = true
        delegate?.keyboardDidShow(notification: notification)
    }

    @objc func keyboardDidHide(notification: NSNotification) {
        isKeyboardShown = false
        delegate?.keyboardDidHide(notification: notification)
    }
}
Vasily Bodnarchuk
  • 19,860
  • 8
  • 111
  • 113
1

If you have multiple textfields say Textfield1, Textfield2, Textfield3 and you want to scroll the scrollview along the y-axis when textfield2 becomes first responder:

if([Textfield2 isFirstResponder])
{
    scrollView.contentOffset = CGPointMake(0,yourY);
} 
Kev
  • 112,868
  • 50
  • 288
  • 373
Deep Batra
  • 112
  • 5
  • 1
    Why would you bother with ```textField1/2/3..``` etc?? If you are using one of the delegate methods like ```textFieldDidBeginEditing:(UITextField *)textField``` then you just use ```textField```. – Supertecnoboff Oct 21 '15 at 09:52
0

As Michael McGuire mentioned in his point #2 above, the system's default behavior misbehaves when the scroll view contains another scroll view between the text field and the scroll view. I've found that the misbehavior also occurs when there's a scroll view merely next to the text field (both embedded in the scroll view that needs to be adjusted to bring the text field into view when the text field wants to start editing. This is on iOS 12.1.

But my solution is different from the above. In my top-level scroll view, which is sub-classed so I can add properties and override methods, I override scrollRectToVisible:animated:. It simply calls its [super scrollRectToVisible:animated:] unless there's a property set that tells it to adjust the rect passed in, which is the frame of the text field. When the property is non-nil, it is a reference to the UITextField in question, and the rect is adjusted so that the scroll view goes further than the system thought it would. So I put this in the UIScrollView's sub-classed header file:

@property (nullable) UITextField *textFieldToBringIntoView;

(with appropriate @synthesize textFieldToBringIntoView; in the implementation. Then I added this override method to the implementation:

- (void)scrollRectToVisible:(CGRect)rect animated:(BOOL)how
{
    if (textFieldToBringIntoView) {
       // Do whatever mucking with `rect`'s origin needed to make it visible
       // based on context or its spatial relationship with the other
       // view that the system is getting confused by.

       textFieldToBringIntoView = nil;        // Go back to normal
       }
    [super scrollRectToVisible:rect animated:how];
}

In the delegate method for the UITextField for when it's about to begin editing, just set textFieldToBringIntoView to the textField in question:

- (BOOL)textFieldShouldBeginEditing:(UITextField *)textField
{
    // Ensure it scrolls into view so that keyboard doesn't obscure it
    // The system is about to call |scrollRectIntoView:| for the scrolling
    // superview, but the system doesn't get things right in certain cases.

    UIScrollView *parent = (UIScrollView *)textField.superview;
    // (or figure out the parent UIScrollView some other way)

    // Tell the override to do something special just once
    // based on this text field's position in its parent's scroll view.
    parent.textFieldToBringIntoView = textField;
    // The override function will set this back to nil

    return(YES);
}

It seems to work. And if Apple fixes their bug, it seems like it might still work (fingers crossed).

jsbox
  • 153
  • 1
  • 9
0

Building off of Vasily Bodnarchuk's answer I created a gist with a simple protocol that you can implement and it'll do it all for you. All you need to do is call registerAsTextDisplacer()

I created a BaseViewController in my project and made that implement it

https://gist.github.com/CameronPorter95/cb68767f5f8052fdc70293c167e9430e

Cameron Porter
  • 619
  • 4
  • 13
0

Other solutions I saw, let you set the offset to the origin of the textField but this makes the scroller view go beyond it bounds. I did this adjustment to the offset instead to not go beyond the bottom nor the top offsets.

Set the keyboardHeightConstraint to the bottom of the page. When the keyboard shows, update its constraint's constant to negative the keyboard height. Then scroll to the responderField as we will show below.

@IBOutlet var keyboardHeightConstraint: NSLayoutConstraint?
var responderField: String?

@objc func keyboardNotification(notification: NSNotification) {
     guard let keyboardValue = notification.userInfo [UIResponder.keyboardFrameEndUserInfoKey] as? NSValue else { return }
     let keyboardHeight = keyboardValue.cgRectValue.height

     keyboardHeightConstraint?.constant = -keyboardHeight
     scroll(field: responderField!)
}

func textFieldDidBeginEditing(_ textField: UITextField) {
     responderField = textField
}

Now we want to make sure we do not scroll greater than the bottom offset nor less than the top offset. At the same time, we want to calculate the offset of the field's maxY value. To do that, we subtract the scrollView.bounds.size.height from the maxY value.

let targetOffset = field.frame.maxY - scrollView.bounds.size.height

I found it nicer to scroll an extra distance of the keyboard height, but you could neglect that if you want to scroll right below the field.

let targetOffset = keyboardHeight + field.frame.maxY - scrollView.bounds.size.height

Remember to add the scrollView.contentInset.bottom if you have the tab bar visible.

func scroll(field: UITextField) {
        guard let keyboardConstraintsConstant = keyboardHeightConstraint?.constant else { return }
        let keyboardHeight = -keyboardConstraintsConstant
        
        view.layoutIfNeeded()
        let bottomOffset = scrollView.contentSize.height - scrollView.bounds.size.height + scrollView.contentInset.bottom
        let topOffset = -scrollView.safeAreaInsets.top
        let targetOffset = keyboardHeight + field.frame.maxY + scrollView.contentInset.bottom - scrollView.bounds.size.height
        let adjustedOffset = targetOffset > bottomOffset ? bottomOffset : (targetOffset < topOffset ? topOffset : targetOffset)
        scrollView.setContentOffset(CGPoint(x: 0, y: adjustedOffset), animated: true)
}