0

Hi I am trying to make a view's bottom align with the top of UIKeyboard.

Update 1: I have created a github project if you would like to give it a try: https://github.com/JCzz/KeyboardProject

Note: I need the aView to be dynamic.

Update 2: Just pushed - to include using frames

I might have been looking at this for too long, I can not wrap my brain around it :-)

Do you know how?

  1. How do I know if the UIKeyboard is on the way down or up?

  2. If UIKeyboard is up, then how to align it with the view(attachKeyboardToFrame - see code).

I have found the following UIView extension:

import UIKit

extension UIView {

    func bindToKeyboard(){
        NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillChange), name: NSNotification.Name.UIKeyboardWillChangeFrame, object: nil)
    }

    func unbindFromKeyboard(){
        NotificationCenter.default.removeObserver(self, name: NSNotification.Name.UIKeyboardWillChangeFrame, object: nil)
    }

    @objc
    func keyboardWillChange(notification: NSNotification) {

        guard let userInfo = notification.userInfo else { return }

        let duration = userInfo[UIKeyboardAnimationDurationUserInfoKey] as! Double
        let curve = userInfo[UIKeyboardAnimationCurveUserInfoKey] as! UInt
        let curFrame = (userInfo[UIKeyboardFrameBeginUserInfoKey] as! NSValue).cgRectValue
        let targetFrame = (userInfo[UIKeyboardFrameEndUserInfoKey] as! NSValue).cgRectValue

        // To get the total height of view
        let topView = UIApplication.shared.windows.last
        //
        let attachKeyboardToFrame = Singleton.sharedInstance.attachKeyboardToFrame
        let global_attachKeyboardToFrame = self.superview?.convert(attachKeyboardToFrame!, to: topView)

        if (targetFrame.height + attachKeyboardToFrame!.height) > (topView?.frame.height)! {
            self.frame.origin.y = -targetFrame.origin.y
        }else{

        }

    }
}
Chris G.
  • 23,930
  • 48
  • 177
  • 302

1 Answers1

2

You can achieve it using following Autolayout solution.

First you need UILayoutGuide that will be used simulate Keyboard aware bottom anchor, and a NSLayoutConstraint that will control this layout guide:

fileprivate let keyboardAwareBottomLayoutGuide: UILayoutGuide = UILayoutGuide()
fileprivate var keyboardTopAnchorConstraint: NSLayoutConstraint!

In the viewDidLoad add the keyboardAwareBottomLayoutGuide to the view and setup the appropriate contraints:

self.view.addLayoutGuide(self.keyboardAwareBottomLayoutGuide)
// this will control keyboardAwareBottomLayoutGuide.topAnchor to be so far from bottom of the bottom as is the height of the presented keyboard
self.keyboardTopAnchorConstraint = self.view.layoutMarginsGuide.bottomAnchor.constraint(equalTo: keyboardAwareBottomLayoutGuide.topAnchor, constant: 0)
self.keyboardTopAnchorConstraint.isActive = true
self.keyboardAwareBottomLayoutGuide.bottomAnchor.constraint(equalTo: view.layoutMarginsGuide.bottomAnchor).isActive = true

Then use following lines to start listening to keyboard showing and hiding:

NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShowNotification(notification:)), name: NSNotification.Name.UIKeyboardWillShow, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHideNotification(notification:)), name: NSNotification.Name.UIKeyboardWillHide, object: nil)

And finally, use following methods to control the keyboardAwareBottomLayoutGuide to mimic the keyboard:

@objc fileprivate func keyboardWillShowNotification(notification: NSNotification) {
    updateKeyboardAwareBottomLayoutGuide(with: notification, hiding: false)
}

@objc fileprivate func keyboardWillHideNotification(notification: NSNotification) {
    updateKeyboardAwareBottomLayoutGuide(with: notification, hiding: true)
}

fileprivate func updateKeyboardAwareBottomLayoutGuide(with notification: NSNotification, hiding: Bool) {
    let userInfo = notification.userInfo

    let animationDuration = (userInfo?[UIKeyboardAnimationDurationUserInfoKey] as? NSNumber)?.doubleValue
    let keyboardEndFrame = (userInfo?[UIKeyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue

    let rawAnimationCurve = (userInfo?[UIKeyboardAnimationCurveUserInfoKey] as? NSNumber)?.uint32Value

    guard let animDuration = animationDuration,
        let keybrdEndFrame = keyboardEndFrame,
        let rawAnimCurve = rawAnimationCurve else {
            return
    }

    let convertedKeyboardEndFrame = view.convert(keybrdEndFrame, from: view.window)

    let rawAnimCurveAdjusted = UInt(rawAnimCurve << 16)
    let animationCurve = UIViewAnimationOptions(rawValue: rawAnimCurveAdjusted)

    // this will move the topAnchor of the keyboardAwareBottomLayoutGuide to height of the keyboard
    self.keyboardTopAnchorConstraint.constant = hiding ? 0 : convertedKeyboardEndFrame.size.height

    self.view.setNeedsLayout()

    UIView.animate(withDuration: animDuration, delay: 0.0, options: [.beginFromCurrentState, animationCurve], animations: {
        self.view.layoutIfNeeded()
    }, completion: { success in
        //
    })
}

Now with all this set up, you can use Autolayout to constraint your views to keyboardAwareBottomLayoutGuide.topAnchor instead of self.view.layoutMarginsGuide.bottomAnchor (or self.view.bottomAnchor, whichever you use). keyboardAwareBottomLayoutGuide will automatically adjust to the keyboard showed or hidden.

Example:

uiTextField.bottomAnchor.constraint(equalTo: keyboardAwareBottomLayoutGuide.topAnchor).isActive = true

EDIT: Directly setting frames

While I strongly recommend using Autolayout, in cases when you cannot go with this, directly setting frames can be also a solution. You can use the same principle. In this approach you don't need layout guide, so you don't need any additional instance properties. Just use viewDidLoad to register for listening notifications:

NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShowNotification(notification:)), name: NSNotification.Name.UIKeyboardWillShow, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHideNotification(notification:)), name: NSNotification.Name.UIKeyboardWillHide, object: nil)

Then implement methods that will react to these notifications:

@objc fileprivate func keyboardWillShowNotification(notification: NSNotification) {
    adjustToKeyboard(with: notification, hiding: false)
}

@objc fileprivate func keyboardWillHideNotification(notification: NSNotification) {
    adjustToKeyboard(with: notification, hiding: true)
}

fileprivate func adjustToKeyboard(with notification: NSNotification, hiding: Bool) {
    let userInfo = notification.userInfo

    let animationDuration = (userInfo?[UIKeyboardAnimationDurationUserInfoKey] as? NSNumber)?.doubleValue
    let keyboardEndFrame = (userInfo?[UIKeyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue

    let rawAnimationCurve = (userInfo?[UIKeyboardAnimationCurveUserInfoKey] as? NSNumber)?.uint32Value

    guard let animDuration = animationDuration,
        let keybrdEndFrame = keyboardEndFrame,
        let rawAnimCurve = rawAnimationCurve else {
            return
    }

    let convertedKeyboardEndFrame = view.convert(keybrdEndFrame, from: view.window)

    let rawAnimCurveAdjusted = UInt(rawAnimCurve << 16)
    let animationCurve = UIViewAnimationOptions(rawValue: rawAnimCurveAdjusted)

    // we will go either up or down depending on whether the keyboard is being hidden or shown
    let diffInHeight = hiding ? convertedKeyboardEndFrame.size.height : -convertedKeyboardEndFrame.size.height

    UIView.animate(withDuration: animDuration, delay: 0.0, options: [.beginFromCurrentState, animationCurve], animations: {
        // this will move the frame of the aView according to the diffInHeight calculated above
        // of course here you need to set all the frames that would be affected by the keyboard (this is why I prefer using autolayout)
        self.aView?.frame = (self.aView?.frame.offsetBy(dx: 0, dy: diff))!

        // of course, you can do anything more complex than just moving the aView up..
    })
}

In both cases, don't forget to unregister observing the notifications once the viewController is deinitialized to prevent retain cycles:

deinit {
    NotificationCenter.default.removeObserver(self, name: NSNotification.Name.UIKeyboardWillShow, object: nil)
    NotificationCenter.default.removeObserver(self, name: NSNotification.Name.UIKeyboardWillHide, object: nil)
}
Milan Nosáľ
  • 19,169
  • 4
  • 55
  • 90
  • Thanks. Will this work even the two views has different parents? – Chris G. Sep 15 '17 at 12:29
  • what two views? I'm not really sure I understand what you are trying to say.. my solution only creates an artificial anchor that adjusts to keyboard.. so instead of laying the views according to left, right, top and bottom you lay it out according to left, right, top and the keyboardAwareBottomLayoutGuide.topAnchor. in other words use autolayout as usually, just instead of using viewController.view.bottomAnchor use keyboardAwareBottomLayoutGuide.topAnchor. – Milan Nosáľ Sep 15 '17 at 13:08
  • so whatever you want to constrain in your view, you do it as you would do it normally, you just use the new layout guide instead of static view.bottomAnchor – Milan Nosáľ Sep 15 '17 at 13:08
  • of course, your autolayout constraints can be applied on any two views, regardless if they have same `superView` or not. However, they have to have a common ancestor - so they have to be in the same view hierarchy – Milan Nosáľ Sep 15 '17 at 13:10
  • 1
    if you are not used to autolayout, you can do layout programmatically instead of setting `self.keyboardTopAnchorConstraint.constant = hiding ? 0 : convertedKeyboardEndFrame.size.height` (`convertedKeyboardEndFrame.size.height` gives you the `CGFloat` number that tells you how much less space you have because of the keyboard). However, I would recommend to embrace autolayout, because it significantly simplifies things.. – Milan Nosáľ Sep 15 '17 at 13:12
  • Thanks a lot, but I can not get it to work. I have created a sample project and pushed to github if you like to give it a go: https://github.com/JCzz/KeyboardProject – Chris G. Sep 15 '17 at 14:49
  • In this project, yes. I have many views that are created in a Framework using Interface Builder xib files. I wish it was different :-) – Chris G. Sep 15 '17 at 15:00
  • Just pushed - to include using frames – Chris G. Sep 15 '17 at 15:10
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/154546/discussion-between-milan-nosa-and-chris-g). – Milan Nosáľ Sep 15 '17 at 15:15
  • Nope, but I think I am about to have something - I will post it when it works. Thanks – Chris G. Sep 15 '17 at 18:09
  • 1
    Hello there - I ended up using RxKeyboard. – Chris G. Sep 17 '17 at 08:09
  • Thanks for the notice, I have to check that frames solution.. Cause it should work :) – Milan Nosáľ Sep 17 '17 at 12:33
  • This is hands down the best answer on handling keyboard showing. Thank you. – b.lyte Feb 19 '20 at 22:03