25

I am building a chat app. I have to move a textfield when keyboard appears. I am doing this with the following code:

func keyboardWillShow(notification: NSNotification) {
    if let userInfo = notification.userInfo {
        if let keyboardSize =  (userInfo[UIKeyboardFrameBeginUserInfoKey] as? NSValue)?.CGRectValue() {
            kbHeight = keyboardSize.height
            self.animateTextField(true)
        }
    }
}
func keyboardWillHide(notification: NSNotification) {
    self.animateTextField(false)
}

func animateTextField(up: Bool) {
    var movement = (up ? -kbHeight : kbHeight)

    UIView.animateWithDuration(0.3, animations: {
        self.view.frame = CGRectOffset(self.view.frame, 0, movement)
    })
}

But when I use this code, the first messages doesn't show. I guess I have to resize the tableview.

Here are screenshots Before and After the keyboard appears:

I am using auto layout.

How can I resolve this problem?

Glorfindel
  • 21,988
  • 13
  • 81
  • 109
Okan
  • 269
  • 1
  • 3
  • 6

5 Answers5

18

2020 UPDATE

Correctly using a constraint...

There is only one way to properly handle this mess in iOS.

  1. Paste KUIViewController below in to your project,

  2. Create a constraint which is very simply to the "bottom of your content".

  3. Drag that constraint to bottomConstraintForKeyboard

KUIViewController will automatically and correctly resize your content view at all times.

Absolutely everything is totally automatic.

All Apple behaviors are handled correctly in the standard way, such as dismissing by taps, etc etc.

You are 100% completely done.

So "which view should you resize?"

You can not use .view ...

Because ... you cannot resize the .view in iOS!!!!!! Doh!

Simply make a UIView named "holder". It sits inside .view.

Put everything of yours inside "holder".

Holder will of course have four simple constraints top/bottom/left/right to .view.

That bottom constraint to "holder" is indeed bottomConstraintForKeyboard.

You're done.

Send a bill to the cliient and go drinking.

There is nothing more to do.

class KUIViewController: UIViewController {

    // KBaseVC is the KEYBOARD variant BaseVC. more on this later

    @IBOutlet var bottomConstraintForKeyboard: NSLayoutConstraint!

    @objc func keyboardWillShow(sender: NSNotification) {
        let i = sender.userInfo!
        let s: TimeInterval = (i[UIResponder.keyboardAnimationDurationUserInfoKey] as! NSNumber).doubleValue
        let k = (i[UIResponder.keyboardFrameEndUserInfoKey] as! NSValue).cgRectValue.height
        bottomConstraintForKeyboard.constant = k
        // Note. that is the correct, actual value. Some prefer to use:
        // bottomConstraintForKeyboard.constant = k - bottomLayoutGuide.length
        UIView.animate(withDuration: s) { self.view.layoutIfNeeded() }
    }

    @objc func keyboardWillHide(sender: NSNotification) {
        let info = sender.userInfo!
        let s: TimeInterval = (info[UIResponder.keyboardAnimationDurationUserInfoKey] as! NSNumber).doubleValue
        bottomConstraintForKeyboard.constant = 0
        UIView.animate(withDuration: s) { self.view.layoutIfNeeded() }
    }

    @objc func clearKeyboard() {
        view.endEditing(true)
        // (subtle iOS bug/problem in obscure cases: see note below
        // you may prefer to add a short delay here)
    }

    func keyboardNotifications() {
        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 viewDidLoad() {
        super.viewDidLoad()
        keyboardNotifications()
        let t = UITapGestureRecognizer(target: self, action: #selector(clearKeyboard))
        view.addGestureRecognizer(t)
        t.cancelsTouchesInView = false
    }
}

Simply ...

Use KUIViewController anywhere a keyboard might appear.

class AddCustomer: KUIViewController, SomeProtocol {

class EnterPost: KUIViewController {

class EditPurchase: KUIViewController {

On those screens absolutely everything is now completely automatic regarding the keyboard.

You're done.

Phew.


*Minor footnote - background clicks correctly dismiss the keyboard. That includes clicks which fall on your content. This is correct Apple behavior. Any unusual variation from that would take a huge amount of very anti-Apple custom programming.

*Extremely minor footnote - so, any and all buttons on the screen will work 100% correctly every time. However in the INCREDIBLY obscure case of nested (!) container views inside nested (!) scroll views with nested (!) page view containers (!!!!), you may find that a button will seemingly not work. This seems to be basically a (obscure!) problem in current iOS. If you encounter this incredibly obscure problem, fortunately the solution is simple. Looking at the function clearKeyboard(), simply add a short delay, you're done.

@objc func clearKeyboard() {
    DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(100)) {
        self.view.endEditing(true)
    }
}

(A great tip from user @wildcat12 https://stackoverflow.com/a/57698468/294884 )

Fattie
  • 27,874
  • 70
  • 431
  • 719
  • 4
    You know, this should not be so hard! – ICL1901 Jan 23 '17 at 15:49
  • 1
    This is the best answer i have found, very nice. Although it seems to move up too much for me, seems to be the same size of the tab bar. – nicwhitts Aug 08 '17 at 16:32
  • I'm not 100% sure what is causing the problem, but the content 'moves up' too much (appears to be 49 pts too much). But when the keyboard hides it returns to correct position. Im assuming that it has something to do with the tab bar (which is 49pts) Iv just accounted for this which has solved the problem. Most likely a problem iv overlooked but the hack works for this project. I will post a question if it persists, thanks – nicwhitts Aug 08 '17 at 17:25
  • @Fattie Thanks a lot for this!! Just out of curiosity, what i was using before was: " self.view.frame.origin.y -= getKeyBoardHeight(notification) ". And it worked fine until suddenly instead of resizing the view, it started shifting the whole view up. Which seems reasonable to me now that I know that the "view" can't be resized. Still don't know why it would work fine before. It's happened to me twice, this time after updating from XCode 7 to 8, the last time after adding some more viewcontrollers to my program. Maybe I changed something in storyboard? Thanks – Irene Oct 05 '17 at 04:10
  • I'm not sure where the claim about "the standard way, such as dismissing by taps" originates...The only keyboard dismissal action provided by UIKit is on UIScrollView dragging, even then it's turned off by default. In any case, it seems like I can just comment out the tap dismissal logic. – androidguy Sep 04 '20 at 09:12
17

You can create an outlet of the bottom auto layout constraint of your table view.

Then simply use this code:

func keyboardWillShow(sender: NSNotification) {
    let info = sender.userInfo!
    var keyboardSize = (info[UIKeyboardFrameEndUserInfoKey] as! NSValue).cgRectValue.height
    bottomConstraint.constant = keyboardSize - bottomLayoutGuide.length

    let duration: TimeInterval = (info[UIKeyboardAnimationDurationUserInfoKey] as! NSNumber).doubleValue

    UIView.animate(withDuration: duration) { self.view.layoutIfNeeded() }
}

func keyboardWillHide(sender: NSNotification) {
    let info = sender.userInfo!
    let duration: TimeInterval = (info[UIKeyboardAnimationDurationUserInfoKey] as! NSNumber).doubleValue
    bottomConstraint.constant = 0

    UIView.animate(withDuration: duration) { self.view.layoutIfNeeded() }
}

If you have trouble creating the bottom constraint:

In storyboard

  • Select your search bar.
  • At the corner in the lower right you'll see 3 icons. Click the middle one looking like |-[]-|.
  • At the top of that popup, there are 4 boxes. Enter 0 at the one for the bottom.
  • Constraint created!

Now you can drag it to your view controller and add it as an outlet.

Another solution is to set the tableView.contentInset.bottom. But I haven't done that before. If you prefer that, I can try to explain it.

Using inset:

func keyboardWillShow(sender: NSNotification) {
    let info = sender.userInfo!
    let keyboardSize = (info[UIKeyboardFrameEndUserInfoKey] as! NSValue).cgRectValue.height

    tableView.contentInset.bottom = keyboardSize
}

func keyboardWillHide(sender: NSNotification) {
    tableView.contentInset.bottom = 0
}

You can try this code for setting the inset. I haven't tried it myself yet, but it should be something like that.

EDIT: Changed the duration with the advice of nacho4d

Eendje
  • 8,815
  • 1
  • 29
  • 31
  • Second solution doesn't work. It doesn't do anything. I think I have to use first solution but I need more info for creating bottom constraint. – Okan Jun 07 '15 at 09:21
  • Textbox shows up under the keyboard, do you know how can I move it? – Okan Jun 07 '15 at 09:26
  • The inset won't be applied to the text box so you'll probably have to use the constraints instead. If it's just the table view, then changing the inset would be enough. Is this text box inside a table view cell? – Eendje Jun 07 '15 at 09:28
  • Well, from the looks of it... Time to use the constraints solution ;) – Eendje Jun 07 '15 at 09:31
  • 1
    instead of hardcoding the animation duration 0.5 you should use the provided length `info[UIKeyboardAnimationDurationUserInfoKey]`. There are sometimes where the change happens without animation – nacho4d Jun 07 '15 at 09:33
  • @nacho4d Thanks, was too lazy and copy pasted some code :p – Eendje Jun 07 '15 at 09:38
  • if `view` is that search bar, then it looks ok. Somehow I doubt `view` is your search bar though. Unless you created that search bar manually with a `View`? – Eendje Jun 07 '15 at 10:03
  • No, I have a view and textfield inside this view. – Okan Jun 07 '15 at 10:29
  • `inset` solution works great (I made it with ObjC), you should also add `UIView.animateWithDuration()` / `[UIView animateWithDuration:]` – Dmitry Isaev Jul 13 '16 at 13:47
  • @JoeBlow: Unless I'm unaware of a change in consensus, edits that add entire new snippets of "latest version" code should be added as new answers, **not** as edits; http://meta.stackoverflow.com/questions/339024/are-edits-that-insert-swift-3-code-into-existing-swift-2-answers-acceptable. Unless there is meta post which supersedes this one, please refrain from such edits. – Matt Jan 23 '17 at 13:16
  • 1
    Updated the code for Swift 3. @JoeBlow: I don't think an (old) answer should be voted down just because it hasn't been updated to the latest version of the language. You could either leave a comment or **edit** the answer correcting the **minor** changes and be done with it (instead of replacing the full post). I think it's an insult to people who have been putting lots of effort and time in helping others and the community by down voting a perfect valid answer just because it's not up to date to the latest version. – Eendje Jan 23 '17 at 15:46
2

From @Fattie 's message:

A detail - (unfortunately) clicks on your content will also dismiss the keyboard. (They both get the event.) However, this is almost always correct behavior; give it a try. There is no reasonable was to avoid this, so forget about it and go with the Apple-flow.

This can be solved by implementing the following UIGestureRecognizerDelegate's method:

func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
        return !(touch.view?.isKind(of: UIControl.self) ?? true)
    }

That way, if the user touches any UIControl (UIButton, UITextField, etc.) the gesture recognizer won't call the clearKeyboard() method.

For this to work, remember to subclass UIGestureRecognizerDelegate either at class definition or with an extension. Then, in viewDidLoad() you should assign the gesture recognizer delegate as self.


Ready to copy and paste code:

// 1. Subclass UIGestureRecognizerDelegate
class KUIViewController: UIViewController, UIGestureRecognizerDelegate {

@IBOutlet var bottomConstraintForKeyboard: NSLayoutConstraint!

func keyboardWillShow(sender: NSNotification) {
    let i = sender.userInfo!
    let k = (i[UIKeyboardFrameEndUserInfoKey] as! NSValue).cgRectValue.height
    bottomConstraintForKeyboard.constant = k - bottomLayoutGuide.length
    let s: TimeInterval = (i[UIKeyboardAnimationDurationUserInfoKey] as! NSNumber).doubleValue
    UIView.animate(withDuration: s) { self.view.layoutIfNeeded() }
}

func keyboardWillHide(sender: NSNotification) {
    let info = sender.userInfo!
    let s: TimeInterval = (info[UIKeyboardAnimationDurationUserInfoKey] as! NSNumber).doubleValue
    bottomConstraintForKeyboard.constant = 0
    UIView.animate(withDuration: s) { self.view.layoutIfNeeded() }
}

func keyboardNotifications() {
    NotificationCenter.default.addObserver(self,
        selector: #selector(keyboardWillShow),
        name: Notification.Name.UIKeyboardWillShow,
        object: nil)
    NotificationCenter.default.addObserver(self,
        selector: #selector(keyboardWillHide),
        name: Notification.Name.UIKeyboardWillHide,
        object: nil)
}

func clearKeyboard() {
    view.endEditing(true)
}

override func viewDidLoad() {
    super.viewDidLoad()
    keyboardNotifications()
    let t = UITapGestureRecognizer(target: self, action: #selector(clearKeyboard))
    view.addGestureRecognizer(t)
    t.cancelsTouchesInView = false

    // 2. Set the gesture recognizer's delegate as self
    t.delegate = self
}

// 3. Implement this method from UIGestureRecognizerDelegate
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
    return !(touch.view?.isKind(of: UIControl.self) ?? true)
}
}
iPruch
  • 31
  • 1
  • 4
2

Maybe it will help somebody. You can achieve the desired behavior without using interface builder at all

First of all you will need to create a constraint and calculate safe area insets in order to support buttonless devices properly

var container: UIView!
var bottomConstraint: NSLayoutConstraint!
let safeInsets = UIApplication.shared.windows[0].safeAreaInsets

then initialize it somewhere in your code

container = UIView()
bottomConstraint = container.bottomAnchor.constraint(equalTo: view.bottomAnchor)

attach it to view and activate

view.addSubview(container)

NSLayoutConstraint.activate([
       ...

       container.leadingAnchor.constraint(equalTo: view.leadingAnchor),
       container.trailingAnchor.constraint(equalTo: view.trailingAnchor),
       container.topAnchor.constraint(equalTo: view.topAnchor),
       bottomConstraint,

       ...
 ])

and finally

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

       if bottomConstraint.constant == 0 {
          bottomConstraint.constant = -keyboardSize.height + safeInsets.bottom     
          view.layoutIfNeeded()
       }
    }
}

@objc func keyboardWillHide(notification: NSNotification) {
       bottomConstraint.constant = 0
       view.layoutIfNeeded()
}

Also if your view is something scrollable and you want to move it up with keyboard and return to initial position as the keyboard hides, you can change view's contentOffset

view.contentOffset = CGPoint(x: view.contentOffset.x, y: view.contentOffset.y + keyboardSize.height - safeInsets.bottom)

for scrolling up, and

view.contentOffset = CGPoint(x: view.contentOffset.x, y: view.contentOffset.y - keyboardSize.height + safeInsets.bottom)

to move it down

dywp
  • 123
  • 2
  • 10
1

if you don't want to fight with this yourself you might find the TPKeyboardAvoiding framework useful

Simply just following the 'installation instructions' i.e. drag and drop the appropriate .h/.m files into your project and then make you ScrollView / TableView a subclass like below:

Custom Class

Chris
  • 7,830
  • 6
  • 38
  • 72
  • But this is for objective c – Okan Jun 07 '15 at 10:28
  • I've used it in a swift app... ObjC bridges jsut fine with swift – Chris Jun 07 '15 at 10:30
  • If you read the docs on the page it will instruct you how to set it up. But you basically make your TableView a subclass of TPKeyboardAvoidingTableView. (updated my answer with an image. – Chris Jun 07 '15 at 10:41
  • Then what will happen to textfield,send button and eye icon? – Okan Jun 07 '15 at 10:45
  • It should all follow accordingly.. Try it out and see what happens – Chris Jun 07 '15 at 10:46
  • TPKeyboardAvoiding framework used to be fantastic, but the only way to go these days is just using a constraint, as explained above. Couldn't be easier. – Fattie Jan 20 '17 at 16:11