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
}
}