1

Description

I have a few UITextField which I want to scroll up, when one of them is covered by keyboard during edition. There are a tons of answers here on SO with many flavors: moving a view (by changing its frame), modifying a constraint, using UIScrollView and UITableView, or using UIScrollView and modifying contentInset.

I decided to use the last one. This one is also described by Apple, and has a Swift version on SO as well as being described on this blog including a sample project on the GitHub.

Partial code

override func viewDidLoad() {
    super.viewDidLoad()
    NSNotificationCenter.defaultCenter().addObserver(self, selector: "keyboardWillShow:", name: UIKeyboardWillShowNotification, object: nil)
    NSNotificationCenter.defaultCenter().addObserver(self, selector: "keyboardWillBeHidden:", name: UIKeyboardWillHideNotification, object: nil)
}

func textFieldShouldReturn(textField: UITextField) -> Bool {
    textField.resignFirstResponder()
    return true
}

func textFieldDidEndEditing(textField: UITextField) {
    self.activeField = nil
}

func textFieldDidBeginEditing(textField: UITextField) {
    self.activeField = textField
}

func keyboardWillShow(notification: NSNotification) {
    if let activeField = self.activeField, keyboardSize = (notification.userInfo?[UIKeyboardFrameBeginUserInfoKey] as? NSValue)?.CGRectValue() {
        let contentInsets = UIEdgeInsets(top: 0.0, left: 0.0, bottom: keyboardSize.height, right: 0.0)
        self.scrollView.contentInset = contentInsets
        self.scrollView.scrollIndicatorInsets = contentInsets
        var aRect = self.view.frame
        aRect.size.height -= keyboardSize.size.height
        if (!CGRectContainsPoint(aRect, activeField.frame.origin)) {
            self.scrollView.scrollRectToVisible(activeField.frame, animated: true)
        }
    }
}

func keyboardWillBeHidden(notification: NSNotification) {
    let contentInsets = UIEdgeInsetsZero
    self.scrollView.contentInset = contentInsets
    self.scrollView.scrollIndicatorInsets = contentInsets
}

Issue

I want just one simple modification - a little more space between the keyboard and an edited field. Because out of the box it looks like this:

screenshot

I modified CGRect in scrollRectToVisible call, but it changed nothing. What's more, even commenting out the line with scrollRectToVisible had no effect at all - everything worked exactly as before (including scrolling up the content). Checked on iOS 9.2

Replicating, if needed, is trivial - just download the working code using the GitHub link above and comment out the scrollRectToVisible line.

Tested workarounds

Workarounds I tried, but didn't like the final effect:

  • Increasing contentInset - user could scroll up more then the contentSize
  • Replacing UIKeyboardDidShowNotification with UIKeyboardWillShowNotification, add another UIKeyboardDidShowNotification observer with just scrollRectToVisible inside - it works, but then there are two scroll up animations, which doesn't look good.

Questions

  • Why changing contentInset (without scrollRectToVisible call) scrolls content in the scrollView? I've not see in any docs information about such behavior
  • And more important - what to do, to scroll it up a little more, to have some space between an edited text field and the keyboard?

What did I miss? Is there some easy way to fix it? Or maybe it's better to play with scrollView.contentSize, instead of contentInset?

Community
  • 1
  • 1
Mikolaj
  • 802
  • 10
  • 18

1 Answers1

1

I haven't found why scrollRectToVisible doesn't work in the scenario described above, however I found other solution which works. In the mentioned Apple article Managing the Keyboard at the very bottom there is a hint

There are other ways you can scroll the edited area in a scroll view above an obscuring keyboard. Instead of altering the bottom content inset of the scroll view, you can extend the height of the content view by the height of the keyboard and then scroll the edited text object into view.

Solution below is based exactly on extending the height of the content view (scrollView.contentSize). It takes into account orientation change and scrolling back when keyboard is being hidden. Works as required - has some space between the active field and the keyboard, see the image below

screenshot

Working code

I've placed the full working code on the GitHub: ScrollViewOnKeyboardShow

var animateContenetView = true
var originalContentOffset: CGPoint?
var isKeyboardVisible = false

let offset : CGFloat = 18

override func viewDidLoad() {
    super.viewDidLoad()

    for case let textField as UITextField in contentView.subviews {
        textField.delegate = self
    }

    let notificationCenter = NSNotificationCenter.defaultCenter()
    notificationCenter.addObserver(self, selector: #selector(ViewController.keyboardWillBeShown(_:)), name: UIKeyboardWillShowNotification, object: nil)
    notificationCenter.addObserver(self, selector: #selector(ViewController.keyboardWillBeHidden(_:)), name: UIKeyboardWillHideNotification, object: nil)
}


func textFieldShouldReturn(textField: UITextField) -> Bool {
    textField.resignFirstResponder()
    return true
}


func textFieldDidEndEditing(textField: UITextField) {
    self.activeField = nil
}


func textFieldDidBeginEditing(textField: UITextField) {
    self.activeField = textField
}


func keyboardWillBeShown(notification: NSNotification) {
    originalContentOffset = scrollView.contentOffset

    if let activeField = self.activeField, keyboardSize = (notification.userInfo?[UIKeyboardFrameBeginUserInfoKey] as? NSValue)?.CGRectValue() {
        var visibleRect = self.scrollView.bounds
        visibleRect.size.height -= keyboardSize.size.height

        //that's to avoid enlarging contentSize multiple times in case of many UITextFields,
        //when user changes an edited text field
        if isKeyboardVisible == false {
            scrollView.contentSize.height += keyboardSize.height
        }

        //scroll only if the keyboard would cover a bottom edge of an
        //active field (including the given offset)
        let activeFieldBottomY = activeField.frame.origin.y + activeField.frame.size.height + offset
        let activeFieldBottomPoint = CGPoint(x: activeField.frame.origin.x, y: activeFieldBottomY)
        if (!CGRectContainsPoint(visibleRect, activeFieldBottomPoint)) {
            var scrollToPointY = activeFieldBottomY - (self.scrollView.bounds.height - keyboardSize.size.height)
            scrollToPointY = min(scrollToPointY, scrollView.contentSize.height - scrollView.frame.size.height)

            scrollView.setContentOffset(CGPoint(x: 0, y: scrollToPointY), animated: animateContenetView)
        }
    }

    isKeyboardVisible = true
}


func keyboardWillBeHidden(notification: NSNotification) {
    scrollView.contentSize.height = contentView.frame.size.height
    if var contentOffset = originalContentOffset {
        contentOffset.y = min(contentOffset.y, scrollView.contentSize.height - scrollView.frame.size.height)
        scrollView.setContentOffset(contentOffset, animated: animateContenetView)
    }

    isKeyboardVisible = false
}


override func viewWillTransitionToSize(size: CGSize, withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator) {
    coordinator.animateAlongsideTransition(nil) { (_) -> Void in
        self.scrollView.contentSize.height = self.contentView.frame.height
    }
}


deinit {
    let notificationCenter = NSNotificationCenter.defaultCenter()
    notificationCenter.removeObserver(self, name: UIKeyboardWillShowNotification, object: nil)
    notificationCenter.removeObserver(self, name: UIKeyboardWillHideNotification, object: nil)
}
Mikolaj
  • 802
  • 10
  • 18