41

I have a table view with a text field and a textview. I've implemented this code like suggested by this apple sample code https://developer.apple.com/library/ios/documentation/StringsTextFonts/Conceptual/TextAndWebiPhoneOS/KeyboardManagement/KeyboardManagement.html

@IBOutlet var myTableView: UITableView
func keyboardWasShown (notification: NSNotification)
{
    println("keyboard was shown")
    var info = notification.userInfo
    var keyboardSize = info.objectForKey(UIKeyboardFrameBeginUserInfoKey).CGRectValue().size

    myTableView.contentInset = UIEdgeInsetsMake(0, 0, keyboardSize.height, 0)
    myTableView.scrollIndicatorInsets = myTableView.contentInset
}

func keyboardWillBeHidden (notification: NSNotification)
{
    println("keyboard will be hidden")
    myTableView.contentInset = UIEdgeInsetsZero
    myTableView.scrollIndicatorInsets = UIEdgeInsetsZero
}
  override func viewDidLoad() {

    super.viewDidLoad()

    NSNotificationCenter.defaultCenter().addObserver(self, selector: "keyboardWasShown:", name: UIKeyboardDidShowNotification, object: nil)
    NSNotificationCenter.defaultCenter().addObserver(self, selector: "keyboardWillBeHidden:", name: UIKeyboardWillHideNotification, object: nil)

}

When i click on the "text" of the scroll view go just above the top of the screen, but when i release the keyboard it remains scrolled up. It's just like the insets property can't be modified after the first time. What's my mistake?

msalafia
  • 2,703
  • 5
  • 23
  • 34

12 Answers12

69
override func viewDidLoad() {
    super.viewDidLoad()

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

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

}

func keyboardWillShow(_ notification:Notification) {

    if let keyboardSize = (notification.userInfo?[UIKeyboardFrameBeginUserInfoKey] as? NSValue)?.cgRectValue {
        tableView.contentInset = UIEdgeInsetsMake(0, 0, keyboardSize.height, 0)
    }
}
func keyboardWillHide(_ notification:Notification) {

    if let keyboardSize = (notification.userInfo?[UIKeyboardFrameBeginUserInfoKey] as? NSValue)?.cgRectValue {
        tableView.contentInset = UIEdgeInsetsMake(0, 0, 0, 0)
    }
}
saurabh
  • 6,687
  • 7
  • 42
  • 63
Zany
  • 893
  • 1
  • 7
  • 5
  • This is the best answer. Thanks. – Ariel Antonio Fundora Jan 09 '18 at 14:47
  • 3
    Plz also remove notification observer – Gajendra Rawat Nov 20 '18 at 17:34
  • 4
    I noticed that you may receive wrong height if you first close the keyboard and then open it again. To avoid this, simply use UIKeyboardFrameEndUserInfoKey instead of UIKeyboardFrameBeginUserInfoKey. – ooxio Jan 31 '19 at 19:51
  • 1
    Best answer, can I also suggest to deinit the notifications, please. deinit { NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillShowNotification, object: nil) NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillHideNotification, object: nil) } – captain_haddock Nov 22 '19 at 13:49
  • Note that removing observers is no longer required: https://stackoverflow.com/a/40339926/1650180 – nickdnk Jul 23 '21 at 20:11
30

For Swift 4.2

In viewDidLoad() of your UIViewController:

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

And implementation of selectors:

@objc private func keyboardWillShow(notification: NSNotification) {
    if let keyboardSize = (notification.userInfo?[UIResponder.keyboardFrameBeginUserInfoKey] as? NSValue)?.cgRectValue {
        tableView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: keyboardSize.height, right: 0)
    }
}

@objc private func keyboardWillHide(notification: NSNotification) {
    tableView.contentInset = .zero
}
Vitalik Kizlov
  • 385
  • 5
  • 9
21

Try keeping the editing index path editingIndexPath Getting index path and scroll tableview to that index path

func keyboardWasShown (notification: NSNotification)
    {
        println("keyboard was shown")
        var info = notification.userInfo
        var keyboardSize = info.objectForKey(UIKeyboardFrameBeginUserInfoKey).CGRectValue().size

        var contentInsets:UIEdgeInsets

        if UIInterfaceOrientationIsPortrait(UIApplication.sharedApplication().statusBarOrientation) {

            contentInsets = UIEdgeInsetsMake(0.0, 0.0, keyboardSize.height, 0.0);
        }
        else {
            contentInsets = UIEdgeInsetsMake(0.0, 0.0, keyboardSize.width, 0.0);

        }

        myTableView.contentInset = contentInsets

        myTableView.scrollToRowAtIndexPath(editingIndexPath, atScrollPosition: .Top, animated: true)
        myTableView.scrollIndicatorInsets = myTableView.contentInset
    }
Community
  • 1
  • 1
Yatheesha
  • 10,412
  • 5
  • 42
  • 45
  • It doesn't work! it still remain fixed after first scroll. I don't understand why the assignments at keyboardWillBeHidden function doesnt work. Is it possible because the tableview is built with Interface Builder and maybe there's some particular option? – msalafia Jul 02 '14 at 11:46
  • @Andorath Can you pass me the sample ? – Yatheesha Jul 02 '14 at 12:11
  • how can i sand you the sample? – msalafia Jul 02 '14 at 13:25
  • I am facing the same issue. Can anyone suggest. – Nupur Gupta Dec 18 '17 at 10:43
  • Please check this link. I hope it will help you https://code.tutsplus.com/tutorials/ios-sdk-keeping-content-from-underneath-the-keyboard--mobile-6103 – Disha Jul 04 '18 at 06:31
21

For Swift 5.0 and considering iPhone Predictive Text is On

take "keyboardSize.height + tableView.rowHeight" as the bottom of tableview just in case if iPhone Predictive Text is On. In that case, we need to scroll up tableview little more.

    override func viewDidLoad() {
        super.viewDidLoad()
        
        tableView.delegate = self
        tableView.dataSource = self
        
        NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow(notification:)), name: UIResponder.keyboardWillShowNotification, object: nil)
        NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide(notification:)), name: UIResponder.keyboardWillHideNotification, object: nil)
    }
    
    @objc private func keyboardWillShow(notification: NSNotification) {
        if let keyboardSize = (notification.userInfo?[UIResponder.keyboardFrameBeginUserInfoKey] as? NSValue)?.cgRectValue {
            tableView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: keyboardSize.height + tableView.rowHeight, right: 0)
        }
    }

    @objc private func keyboardWillHide(notification: NSNotification) {
        tableView.contentInset = .zero
    }
Community
  • 1
  • 1
patel dhruval
  • 1,002
  • 10
  • 12
  • 1
    This doesn't seem to perform well for me. The first time the keyboard shows it works, but the second time, the value returned by keyboardSize.height is like 100 less. – paul_f Nov 23 '21 at 00:01
  • @paul_f it is because of keyboardFrameBeginUserInfoKey. To get the "correct" (final) height use keyboardFrameEndUserInfoKey. – Thel Jun 26 '23 at 18:35
20

Use this Awesome Extension (Updated for Swift 4.2),

extension UIViewController {

    func registerForKeyboardWillShowNotification(_ scrollView: UIScrollView, usingBlock block: ((CGSize?) -> Void)? = nil) {
        _ = NotificationCenter.default.addObserver(forName: UIResponder.keyboardWillShowNotification, object: nil, queue: nil, using: { notification -> Void in
            let userInfo = notification.userInfo!
            let keyboardSize = (userInfo[UIResponder.keyboardFrameEndUserInfoKey]! as AnyObject).cgRectValue.size
            let contentInsets = UIEdgeInsets(top: scrollView.contentInset.top, left: scrollView.contentInset.left, bottom: keyboardSize.height, right: scrollView.contentInset.right)

            scrollView.setContentInsetAndScrollIndicatorInsets(contentInsets)
            block?(keyboardSize)
        })
    }

    func registerForKeyboardWillHideNotification(_ scrollView: UIScrollView, usingBlock block: ((CGSize?) -> Void)? = nil) {
        _ = NotificationCenter.default.addObserver(forName: UIResponder.keyboardWillHideNotification, object: nil, queue: nil, using: { notification -> Void in
            let userInfo = notification.userInfo!
            let keyboardSize = (userInfo[UIResponder.keyboardFrameEndUserInfoKey]! as AnyObject).cgRectValue.size
            let contentInsets = UIEdgeInsets(top: scrollView.contentInset.top, left: scrollView.contentInset.left, bottom: 0, right: scrollView.contentInset.right)

            scrollView.setContentInsetAndScrollIndicatorInsets(contentInsets)
            block?(keyboardSize)
        })
    }
}

extension UIScrollView {

    func setContentInsetAndScrollIndicatorInsets(_ edgeInsets: UIEdgeInsets) {
        self.contentInset = edgeInsets
        self.scrollIndicatorInsets = edgeInsets
    }
}

and use as mentioned below from the respective ViewController,

@IBOutlet weak var tableview: UITableView!

override func viewDidLoad() {
        super.viewDidLoad()
        registerForKeyboardWillShowNotification(tableview)
        registerForKeyboardWillHideNotification(tableview)

        /* use the above functions with
           block, in case you want the trigger just after the keyboard
           hide or show which will return you the keyboard size also.
         */

        registerForKeyboardWillShowNotification(tableView) { (keyboardSize) in
            print("size 1 - \(keyboardSize!)")
        }
        registerForKeyboardWillHideNotification(tableView) { (keyboardSize) in
            print("size 2 - \(keyboardSize!)")
        }

    }
Soumen
  • 2,070
  • 21
  • 25
  • Nice extension. Please update to Swift 4.2 and show an example using the block.Thx. – Peter Kreinz Feb 04 '19 at 13:45
  • 1
    Updated the answer for Swift 4.2 – Soumen Feb 05 '19 at 10:33
  • There's a missing `}` there before `extension UIScrollView` (and the indentation is slightly off) – John J. Camilleri Feb 28 '19 at 08:32
  • 1
    Really great extension, but does anyone else notice that the value for the bottom edge inset isn't quite right? I end up with extra dead space between my view and the keyboard (see [here](https://imgur.com/a/VvlaFLg)). The view is aligned to the safe area. I have to adjust the `keyboardSize.height` value with the function `return height - (height > 200 ? 50 : 30)`. – John J. Camilleri Feb 28 '19 at 09:14
  • 1
    Take note to remove the observers or there might be a memory leak. See: https://developer.apple.com/library/archive/releasenotes/Foundation/RN-FoundationOlderNotes/index.html#:~:text=Block%20based%20observers,is%20still%20supported. – CyberMew Feb 15 '22 at 05:27
  • Great! Might also want to account for safeAreaInsets. – deeJ Nov 07 '22 at 17:59
12

Use below code to get Indexpath and change UITableview content Offset based on UIKeyboard Height

func keyboardWillShow(notification: NSNotification) {
    if ((notification.userInfo?[UIKeyboardFrameBeginUserInfoKey] as? NSValue)?.cgRectValue) != nil {
        //self.view.frame.origin.y -= keyboardSize.height
        var userInfo = notification.userInfo!
        var keyboardFrame:CGRect = (userInfo[UIKeyboardFrameEndUserInfoKey] as! NSValue).cgRectValue
        keyboardFrame = self.view.convert(keyboardFrame, from: nil)

        var contentInset:UIEdgeInsets = self.tbl.contentInset
        contentInset.bottom = keyboardFrame.size.height
        self.tbl.contentInset = contentInset

        //get indexpath
        let indexpath = NSIndexPath(forRow: 1, inSection: 0)
        self.tbl.scrollToRowAtIndexPath(indexpath, atScrollPosition: UITableViewScrollPosition.Top, animated: true)
    }
}

func keyboardWillHide(notification: NSNotification) {
    if ((notification.userInfo?[UIKeyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue) != nil {
        let contentInset:UIEdgeInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
        self.tbl.contentInset = contentInset
    }
}
Hardik Thakkar
  • 15,269
  • 2
  • 94
  • 81
7

The easiest way is to simply set

tableView.keyboardDismissMode = .onDrag

This will work out of the box.

Myrenkar
  • 95
  • 1
  • 4
4

** not scrolling to an upside on keyboard show**

    override func viewDidLoad() {
    super.viewDidLoad()

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

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

func keyboardWillShow(_ notification:Notification) {

    if let keyboardSize = (notification.userInfo?[UIKeyboardFrameBeginUserInfoKey] as? NSValue)?.cgRectValue {
        tableView.contentInset = UIEdgeInsetsMake(0, 0, keyboardSize.height, 0)
    }
}

func keyboardWillHide(_ notification:Notification) 
{
    if let keyboardSize = (notification.userInfo?[UIKeyboardFrameBeginUserInfoKey] as? NSValue)?.cgRectValue {
        tableView.contentInset = UIEdgeInsetsMake(0, 0, 0, 0)
    }
}
Furkan
  • 306
  • 4
  • 17
2

I had a small idea about that and I wrote an extension in swift. Feel free to contribute and use it in your own projects:

https://github.com/joluc/AutoAdjust

import Foundation
import UIKit

extension UITableView {
    // I am working on a way to deinit the observers when the tableview also is deiniting.
    // If you have ideas, feel free to help out!
    func setupAutoAdjust()
    {
        NotificationCenter.default.addObserver(self, selector: #selector(self.keyboardshown), name: NSNotification.Name.UIKeyboardWillShow, object: nil)
        NotificationCenter.default.addObserver(self, selector: #selector(self.keyboardhide), name: NSNotification.Name.UIKeyboardWillHide, object: nil)
    }
    @objc func keyboardshown(_ notification:Notification)
    {
        if let keyboardSize = (notification.userInfo?[UIKeyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue {
            self.fitContentInset(inset: UIEdgeInsetsMake(0, 0, keyboardSize.height, 0))
        }
    }
    @objc func keyboardhide(_ notification:Notification)
    {
        if ((notification.userInfo?[UIKeyboardFrameBeginUserInfoKey] as? NSValue)?.cgRectValue) != nil {
            self.fitContentInset(inset: .zero)
        }

    }
    func fitContentInset(inset:UIEdgeInsets!)
    {
        self.contentInset = inset
        self.scrollIndicatorInsets = inset
    }
}
chronikum
  • 716
  • 5
  • 12
2

You can also use textfield/textViewDidBeginEditing. I've used this for a chat log tableView.

func textViewDidBeginEditing(_ textView: UITextView) {

       if self.chatMessages.count >= 1 {
         let section = self.chatMessages.count - 1
         let row = self.chatMessages[section].count - 1
         let indexPath = IndexPath(row: row, section: section )
         self.tableView.scrollToRow(at: indexPath, at: .bottom, animated: true)
       }
}
DisplayName
  • 152
  • 1
  • 4
0
    func keyboardWillShow(notification: NSNotification) {
    if ((notification.userInfo?[UIKeyboardFrameBeginUserInfoKey] as? NSValue)?.CGRectValue()) != nil {
        //self.view.frame.origin.y -= keyboardSize.height
        var userInfo = notification.userInfo!
        var keyboardFrame:CGRect = (userInfo[UIKeyboardFrameBeginUserInfoKey] as! NSValue).CGRectValue()
        keyboardFrame = self.view.convertRect(keyboardFrame, fromView: nil)

        var contentInset:UIEdgeInsets = self.tbl.contentInset
        contentInset.bottom = keyboardFrame.size.height
        self.tbl.contentInset = contentInset

        //get indexpath
        let indexpath = NSIndexPath(forRow: 1, inSection: 0)
        self.tbl.scrollToRowAtIndexPath(indexpath, atScrollPosition: UITableViewScrollPosition.Top, animated: true)
    }
}

func keyboardWillHide(notification: NSNotification) {
    if ((notification.userInfo?[UIKeyboardFrameBeginUserInfoKey] as? NSValue)?.CGRectValue()) != nil {
        let contentInset:UIEdgeInsets = UIEdgeInsetsZero
        self.tbl.contentInset = contentInset
    }
}
Aftab Ahmed
  • 99
  • 1
  • 8
  • The problem with the above approach is the first click on the table view after setting the contentInset, in the section > 0 points to section = 0. i.e. at didSelect the indexPath values correspond to the values in section 0, though you have clicked on the rows in the section > 0. This happens only for the first click. – Aftab Ahmed Mar 15 '18 at 14:02
0
override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)

    NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow), name: UIResponder.keyboardWillShowNotification, 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)
}

@objc func keyboardWillShow(_ notification:Notification) {
    if let keyboardSize = (notification.userInfo?[UIResponder.keyboardFrameBeginUserInfoKey] as? NSValue)?.cgRectValue {
        tableView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: keyboardSize.height, right: 0)
    }
}
@objc func keyboardWillHide(_ notification:Notification) {
    if ((notification.userInfo?[UIResponder.keyboardFrameBeginUserInfoKey] as? NSValue)?.cgRectValue) != nil {
        tableView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
    }
}
Thomas Mary
  • 1,535
  • 1
  • 13
  • 24
  • You can use `tableView.contentInset = .zero` instead of `tableView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)` – laneboyandrew Aug 03 '23 at 11:54