3

I have a UIScrollView and a UITextView, just like in any messaging / chat app, whenUIScrollView is scrolled down, the keypad interactively being dragged too.

I need to detect keyboard height while UIScrollView is scrolled, I tried UIKeyboardWillChangeFrame observer, but this event is called after scroll tap is released.

Without knowing keyboard height, I am unable to update the UITextView bottom constraint, and I get a gap between the keypad and bottom view @screenshot.

enter image description here

Also attaching screenshot from Viber, that does align the bottom bar when keyboard being dragged from scroll bar, also can be seen in WhatsApp too.

enter image description here

AamirR
  • 11,672
  • 4
  • 59
  • 73

4 Answers4

2

As of iOS 10, Apple doesn't provide a NSNotification observer to detect the frame change while the keypad is dragged interactively by UIScrollView, UIKeyboardWillChangeFrame and UIKeyboardDidChangeFrame are observed only once releasing tap.

Anyways, after looking around DAKeyboardControl library, I had the idea to attach UIScrollView.UIPanGestureRecognizer in the UIViewController, so any gesture events that are produced will be handled in UIViewController as well. After screwing around several hours, I got it to work, here is all the code that is necessary for this:

class ViewController: UIViewController, UIGestureRecognizerDelegate {

    fileprivate let collectionView = UICollectionView(frame: .zero)
    private let bottomView = UIView()
    fileprivate var bottomInset: NSLayoutConstraint!

    // This holds height of keypad
    private var maxKeypadHeight: CGFloat = 0 {
        didSet {
            self.updateCollectionViewInsets(maxKeypadHeight + self.bottomView.frame.height)
            self.bottomInset.constant = -maxKeypadHeight
        }
    }

    private var isListeningKeypadChange = false

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

        NotificationCenter.default.addObserver(self, selector: #selector(keypadWillChange(_:)), name: .UIKeyboardWillChangeFrame, object: nil)
        NotificationCenter.default.addObserver(self, selector: #selector(keypadWillShow(_:)), name: .UIKeyboardWillShow, object: nil)
        NotificationCenter.default.addObserver(self, selector: #selector(keypadWillHide(_:)), name: .UIKeyboardWillHide, object: nil)
        NotificationCenter.default.addObserver(self, selector: #selector(keypadDidHide), name: .UIKeyboardDidHide, object: nil)
    }

    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)

        NotificationCenter.default.removeObserver(self)
    }

    func keypadWillShow(_ notification: Notification) {
        guard !self.isListeningKeypadChange, let userInfo = notification.userInfo as? [String : Any],
            let animationDuration = userInfo[UIKeyboardAnimationDurationUserInfoKey] as? TimeInterval,
            let animationCurve = userInfo[UIKeyboardAnimationCurveUserInfoKey] as? UInt,
            let value = userInfo[UIKeyboardFrameEndUserInfoKey] as? NSValue
            else {
                return
        }

        self.maxKeypadHeight = value.cgRectValue.height

        let options = UIViewAnimationOptions.beginFromCurrentState.union(UIViewAnimationOptions(rawValue: animationCurve))
        UIView.animate(withDuration: animationDuration, delay: 0, options: options, animations: { [weak self] in
            self?.view.layoutIfNeeded()
            }, completion: { finished in
                guard finished else { return }

                // Some delay of about 500MS, before ready to listen other keypad events
                DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in
                    self?.beginListeningKeypadChange()
                }
        })
    }

    func handlePanGestureRecognizer(_ pan: UIPanGestureRecognizer) {
        guard self.isListeningKeypadChange, let windowHeight = self.view.window?.frame.height else { return }

        let barHeight = self.bottomView.frame.height
        let keypadHeight = abs(self.bottomInset.constant)
        let usedHeight = keypadHeight + barHeight

        let dragY = windowHeight - pan.location(in: self.view.window).y
        let newValue = min(dragY < usedHeight ? max(dragY, 0) : dragY, self.maxKeypadHeight)

        print("Old: \(keypadHeight)        New: \(newValue)        Drag: \(dragY)        Used: \(usedHeight)")
        guard keypadHeight != newValue else { return }
        self.updateCollectionViewInsets(newValue + barHeight)
        self.bottomInset.constant = -newValue
    }

    func keypadWillChange(_ notification: Notification) {
        if self.isListeningKeypadChange, let value = notification.userInfo?[UIKeyboardFrameEndUserInfoKey] as? NSValue {
            self.maxKeypadHeight = value.cgRectValue.height
        }
    }

    func keypadWillHide(_ notification: Notification) {
        guard let userInfo = notification.userInfo as? [String : Any] else { return }

        self.maxKeypadHeight = 0

        var options = UIViewAnimationOptions.beginFromCurrentState
        if let animationCurve = userInfo[UIKeyboardAnimationCurveUserInfoKey] as? UInt {
            options = options.union(UIViewAnimationOptions(rawValue: animationCurve))
        }

        let duration = userInfo[UIKeyboardAnimationDurationUserInfoKey] as? TimeInterval
        UIView.animate(withDuration: duration ?? 0, delay: 0, options: options, animations: {
            self.view.layoutIfNeeded()
        }, completion: nil)
    }

    func keypadDidHide() {
        self.collectionView.panGestureRecognizer.removeTarget(self, action: nil)
        self.isListeningKeypadChange = false
        if (self.maxKeypadHeight != 0 || self.bottomInset.constant != 0) {
            self.maxKeypadHeight = 0
        }
    }

    private func beginListeningKeypadChange() {
        self.isListeningKeypadChange = true
        self.collectionView.panGestureRecognizer.addTarget(self, action: #selector(self.handlePanGestureRecognizer(_:)))
    }

    fileprivate func updateCollectionViewInsets(_ value: CGFloat) {
        let insets = UIEdgeInsets(top: 0, left: 0, bottom: value + 8, right: 0)
        self.collectionView.contentInset = insets
        self.collectionView.scrollIndicatorInsets = insets
    }        
}
AamirR
  • 11,672
  • 4
  • 59
  • 73
1

You can simply add this pod:

pod 'IQKeyboardManagerSwift'

Then in your AppDelegate.swift add:

import IQKeyboardManagerSwift

And a in the didFinishLaunchingWithOptions function

 func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
    // Override point for customization after application launch.

    IQKeyboardManager.sharedManager().enable = true // ADD THIS !!!

    return true
}

That this simple.

Thomas
  • 1,289
  • 1
  • 17
  • 29
0

swift 3.0

You can try this way, i have implemented in my project. Hope it will help you.

@IBOutlet weak var constant_ViewBottom: NSLayoutConstraint! // 0

override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        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)
    }

   override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        NotificationCenter.default.removeObserver(self, name: NSNotification.Name.UIKeyboardWillShow, object: nil)
        NotificationCenter.default.removeObserver(self, name: NSNotification.Name.UIKeyboardWillHide, object: nil)
    }

func keyboardWillShow(_ notification: NSNotification){
        if let keyboardRectValue = (notification.userInfo?[UIKeyboardFrameBeginUserInfoKey] as? NSValue)?.cgRectValue.size {
            let keyboardHeight = keyboardRectValue.height
            print("keyboardHeight:=\(keyboardHeight)")
            constant_ViewBottom.constant = keyboardHeight
            self.view.layoutIfNeeded()
        }
    }
 func keyboardWillHide(_ notification: NSNotification){
        constant_ViewBottom.constant = 0.0
        self.view.layoutIfNeeded()
    }
Community
  • 1
  • 1
Hitesh
  • 896
  • 1
  • 9
  • 22
0

It seems like you have a wrong constant of bottom constraint. Try to reset bottom constraint everytime and set new height value

    func keyboardDidChangeFrame(notification: Notification) {
        if let userInfo = notification.userInfo {
            if let endFrame = (userInfo[UIKeyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue {
                let duration: TimeInterval = (userInfo[UIKeyboardAnimationDurationUserInfoKey] as? NSNumber)?.doubleValue ?? 0
                let animationCurveRawNSN = userInfo[UIKeyboardAnimationCurveUserInfoKey] as? NSNumber
                let animationCurveRaw = animationCurveRawNSN?.uintValue ?? UIViewAnimationOptions().rawValue
                let animationCurve:UIViewAnimationOptions = UIViewAnimationOptions(rawValue: animationCurveRaw)
                if endFrame.origin.y >= UIScreen.main.bounds.size.height {
                    self.inputBarBottomSpacing.constant = 0
                } else {
                   //the most important logic branch, reset current bottom constant constraint value
                    if self.inputBarBottomSpacing.constant != 0 {
                        self.inputBarBottomSpacing.constant = 0
                    }
                    self.inputBarBottomSpacing.constant = -endFrame.size.height
                }
                UIView.animate(withDuration: duration, delay: TimeInterval(0), options: animationCurve, animations: {
                    self.view.layoutIfNeeded()
                }, completion: nil)
            }
        }
    }