ScrollView and keyboard in Swift

82,316

Solution 1

In ViewDidLoad, register the notifications:

NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow), name:UIResponder.keyboardWillShowNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide), name:UIResponder.keyboardWillHideNotification, object: nil)

Add below observer methods which does the automatic scrolling when keyboard appears.

@objc func keyboardWillShow(notification:NSNotification) {

    guard let userInfo = notification.userInfo else { return }
    var keyboardFrame:CGRect = (userInfo[UIResponder.keyboardFrameBeginUserInfoKey] as! NSValue).cgRectValue
    keyboardFrame = self.view.convert(keyboardFrame, from: nil)

    var contentInset:UIEdgeInsets = self.scrollView.contentInset
    contentInset.bottom = keyboardFrame.size.height + 20
    scrollView.contentInset = contentInset
}

@objc func keyboardWillHide(notification:NSNotification) {

    let contentInset:UIEdgeInsets = UIEdgeInsets.zero
    scrollView.contentInset = contentInset
}

Solution 2

The top answer for swift 3:

NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow), name:NSNotification.Name.UIKeyboardWillShow, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide), name:NSNotification.Name.UIKeyboardWillHide, object: nil)

And then:

func keyboardWillShow(notification:NSNotification){
    //give room at the bottom of the scroll view, so it doesn't cover up anything the user needs to tap
    var userInfo = notification.userInfo!
    var keyboardFrame:CGRect = (userInfo[UIKeyboardFrameBeginUserInfoKey] as! NSValue).cgRectValue
    keyboardFrame = self.view.convert(keyboardFrame, from: nil)

    var contentInset:UIEdgeInsets = self.theScrollView.contentInset
    contentInset.bottom = keyboardFrame.size.height
    theScrollView.contentInset = contentInset
}

func keyboardWillHide(notification:NSNotification){
    let contentInset:UIEdgeInsets = UIEdgeInsets.zero
    theScrollView.contentInset = contentInset
}

Solution 3

Here is a complete solution, utilizing guard and concise code. Plus correct code in keyboardWillHide to only reset the bottom to 0.

@IBOutlet private weak var scrollView: UIScrollView!

override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)
    registerNotifications()
}

override func viewDidDisappear(_ animated: Bool) {
    super.viewDidDisappear(animated)
    scrollView.contentInset.bottom = 0
}

private func registerNotifications() {
    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 private func keyboardWillShow(notification: NSNotification){
    guard let keyboardFrame = notification.userInfo![UIResponder.keyboardFrameEndUserInfoKey] as? NSValue else { return }
    scrollView.contentInset.bottom = view.convert(keyboardFrame.cgRectValue, from: nil).size.height
}

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

Solution 4

for Swift 4.0

In ViewDidLoad

// setup keyboard event
NotificationCenter.default.addObserver(self, selector: #selector(self.keyboardWillShow), name: NSNotification.Name.UIKeyboardWillShow, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(self.keyboardWillHide), name: NSNotification.Name.UIKeyboardWillHide, object: nil)

Add below observer methods which does the automatic scrolling when keyboard appears.

@objc func keyboardWillShow(notification:NSNotification){
    var userInfo = notification.userInfo!
    var keyboardFrame:CGRect = (userInfo[UIKeyboardFrameBeginUserInfoKey] as! NSValue).cgRectValue
    keyboardFrame = self.view.convert(keyboardFrame, from: nil)

    var contentInset:UIEdgeInsets = self.ui_scrollView.contentInset
    contentInset.bottom = keyboardFrame.size.height
    ui_scrollView.contentInset = contentInset
}

@objc func keyboardWillHide(notification:NSNotification){

    let contentInset:UIEdgeInsets = UIEdgeInsets.zero
    ui_scrollView.contentInset = contentInset
}

Solution 5

Swift 5 Only adjust ScrollView when TextField is hidden by keyboard (for multiple TextFields)

Add / Remove Observers:

override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)
    NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow), name: UIResponder.keyboardWillChangeFrameNotification, object: nil)
    NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide), name: UIResponder.keyboardWillHideNotification, object: nil)
}

override func viewWillDisappear(_ animated: Bool) {
    super.viewWillDisappear(animated)
    NotificationCenter.default.removeObserver(self)
}

Keep track of these values so you can return to your original position:

var scrollOffset : CGFloat = 0
var distance : CGFloat = 0

Adjust ScrollView contentOffset depending on TextField Location:

@objc func keyboardWillShow(notification: NSNotification) {
    if let keyboardSize = (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue {

        var safeArea = self.view.frame
        safeArea.size.height += scrollView.contentOffset.y
        safeArea.size.height -= keyboardSize.height + (UIScreen.main.bounds.height*0.04) // Adjust buffer to your liking

        // determine which UIView was selected and if it is covered by keyboard

        let activeField: UIView? = [textFieldA, textViewB, textFieldC].first { $0.isFirstResponder }
        if let activeField = activeField {
            if safeArea.contains(CGPoint(x: 0, y: activeField.frame.maxY)) {
                print("No need to Scroll")
                return
            } else {
                distance = activeField.frame.maxY - safeArea.size.height
                scrollOffset = scrollView.contentOffset.y
                self.scrollView.setContentOffset(CGPoint(x: 0, y: scrollOffset + distance), animated: true)
            }
        }
        // prevent scrolling while typing

        scrollView.isScrollEnabled = false
    }
}
@objc func keyboardWillHide(notification: NSNotification) {
        if distance == 0 {
            return
        }
        // return to origin scrollOffset
        self.scrollView.setContentOffset(CGPoint(x: 0, y: scrollOffset), animated: true)
        scrollOffset = 0
        distance = 0
        scrollView.isScrollEnabled = true
}

Make sure to use [UIResponder.keyboardFrameEndUserInfoKey] to get the proper keyboard height the first time.

Share:
82,316
GalinhaVoadora
Author by

GalinhaVoadora

Updated on July 05, 2022

Comments

  • GalinhaVoadora
    GalinhaVoadora almost 2 years

    I started creating a simple iOS app that does some operations.

    But I'm having some problems when the keyboard appears, hiding one of my textfields.

    I think it's a common problem and I did some research but I couldn't find anything that solved my problem.

    I want to use a ScrollView rather than animate the textfield to make it visible.

  • Engnyl
    Engnyl over 8 years
    Using these code along with scrollView saved my life. Just add 20 contentInset.bottom to scroll it properly. contentInset.bottom = keyboardFrame.size.height + 20
  • Admin
    Admin about 8 years
    Works perfectly! The contentInset can be set to a 'let' instead of a 'var' in the keyboardWillHide though :)
  • makle
    makle over 7 years
    Also maybe keep the textFieldShouldReturn method from above, if you want to use the return key for dismissing the keyboard. Thx for the update!
  • nikans
    nikans about 7 years
    Don't forget to NotificationCenter.default.removeObserver(self)
  • Siddharth
    Siddharth about 7 years
    I get ViewController has no member for self.scrollView
  • KMC
    KMC about 7 years
    @Siddharth That's because you need to create IBOutlet for your scrollview
  • Rivers
    Rivers about 7 years
    Works like a charm. Thanks Daniel.
  • PJayRushton
    PJayRushton about 7 years
    @nikans Apple Docs for NotificationCenter: If your app targets iOS 9.0 and later or macOS 10.11 and later, you don't need to unregister an observer in its deallocation method.
  • crashoverride777
    crashoverride777 almost 7 years
    Nice answer. To be even more concise you can say ".UIKeyboardWillShow" instead of "NSNotification.Name.UIKeyboardWillShow". I updated your answer if you don't mind.
  • Gleb Tarasov
    Gleb Tarasov over 6 years
    I think you need replace UIKeyboardFrameBeginUserInfoKey with UIKeyboardFrameEndUserInfoKey in your answer. Because you need end frame in willShow method.
  • iOS Geek
    iOS Geek almost 6 years
    Awesome , really Thanks
  • finngu
    finngu almost 6 years
    The first time the keyboard opens, its height is zero for me. Afterwards it works fine, though. Do you know of any bugs related to this?
  • Sam
    Sam almost 6 years
    This solution works when the UIControlView is scrolled.
  • Paul Cantrell
    Paul Cantrell over 5 years
    UIKeyboardFrameBeginUserInfoKey is the wrong key; it should be UIKeyboardFrameEndUserInfoKey (begin → end). This solution gives zero height for the keyboard.
  • Hashir Saeed
    Hashir Saeed over 5 years
    This Helped me! Thanks Mate :)
  • Richard
    Richard about 5 years
    Great answer. I think it would be safer to use viewDidDisappear() - if the view is navigated away whilst the keyboard is visible it is conceivable you could remove the notification observer before the hide has actually taken place, and therefore not reset your content inset. To be extra sure I'd also set the inset to 0 whenever the notification observer is removed.
  • Leo
    Leo almost 5 years
    Pretty clean solution. In addition, keyboardWillShow and keyboardWillHide can be private.
  • Elijah
    Elijah almost 5 years
    Thanks @Richard. I have updated my answer accordingly.
  • jayant rawat
    jayant rawat over 4 years
    Binary operator '+' cannot be applied to operands of type 'UIEdgeInsets' and 'Double'
  • Sergey Didanov
    Sergey Didanov over 3 years
    First keyboardWillShow returns correct keyboard height (260). All next keyboardWillShow return keyboard height 216. Why?
  • Ben Shabat
    Ben Shabat over 3 years
    Do we need to remove the observers when ViewController get destroyed?
  • Radu Ursache
    Radu Ursache almost 3 years
    @BenShabat Apple Docs for NotificationCenter: If your app targets iOS 9.0 and later or macOS 10.11 and later, you don't need to unregister an observer in its deallocation method.
  • Roman
    Roman over 2 years
    Awesome solution, man. Works like a charm. But you really, REALLY should have mentioned about NotificationCenter.default.addObserver first. Maybe that's why you still didn't get any likes!
  • Ben
    Ben over 2 years
    For Swift 5, NSNotification.Name.UIKeyboardWillShow is UIResponder.keyboardWillShowNotification and UIKeyboardFrameBeginUserInfoKey is UIResponder.keyboardFrameBeginUserInfoKey.