Extend iOS 11 Safe Area to include the keyboard
What seems to be working for me is to calculate the intersection between view.safeAreaLayoutGuide.layoutFrame
and the keyboard frame, and then setting the height of that as the additionalSafeAreaInsets.bottom
, instead of the whole keyboard frame height. I don't have a toolbar in my view controller, but I do have a tab bar and it is accounted for correctly.
Complete code:
import UIKitpublic extension UIViewController { func startAvoidingKeyboard() { NotificationCenter.default .addObserver(self, selector: #selector(onKeyboardFrameWillChangeNotificationReceived(_:)), name: UIResponder.keyboardWillChangeFrameNotification, object: nil) } func stopAvoidingKeyboard() { NotificationCenter.default .removeObserver(self, name: UIResponder.keyboardWillChangeFrameNotification, object: nil) } @objc private func onKeyboardFrameWillChangeNotificationReceived(_ notification: Notification) { guard let userInfo = notification.userInfo, let keyboardFrame = (userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue else { return } let keyboardFrameInView = view.convert(keyboardFrame, from: nil) let safeAreaFrame = view.safeAreaLayoutGuide.layoutFrame.insetBy(dx: 0, dy: -additionalSafeAreaInsets.bottom) let intersection = safeAreaFrame.intersection(keyboardFrameInView) let keyboardAnimationDuration = notification.userInfo?[UIResponder.keyboardAnimationDurationUserInfoKey] let animationDuration: TimeInterval = (keyboardAnimationDuration as? NSNumber)?.doubleValue ?? 0 let animationCurveRawNSN = notification.userInfo?[UIResponder.keyboardAnimationCurveUserInfoKey] as? NSNumber let animationCurveRaw = animationCurveRawNSN?.uintValue ?? UIView.AnimationOptions.curveEaseInOut.rawValue let animationCurve = UIView.AnimationOptions(rawValue: animationCurveRaw) UIView.animate(withDuration: animationDuration, delay: 0, options: animationCurve, animations: { self.additionalSafeAreaInsets.bottom = intersection.height self.view.layoutIfNeeded() }, completion: nil) }}
If you need support back to pre IOS11 versions you can use the function from Fabio and add:
if #available(iOS 11.0, *) { }
final solution:
extension UIViewController { func startAvoidingKeyboard() { NotificationCenter.default.addObserver(self, selector: #selector(_onKeyboardFrameWillChangeNotificationReceived(_:)), name: NSNotification.Name.UIKeyboardWillChangeFrame, object: nil) } func stopAvoidingKeyboard() { NotificationCenter.default.removeObserver(self, name: NSNotification.Name.UIKeyboardWillChangeFrame, object: nil) } @objc private func _onKeyboardFrameWillChangeNotificationReceived(_ notification: Notification) { if #available(iOS 11.0, *) { guard let userInfo = notification.userInfo, let keyboardFrame = (userInfo[UIKeyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue else { return } let keyboardFrameInView = view.convert(keyboardFrame, from: nil) let safeAreaFrame = view.safeAreaLayoutGuide.layoutFrame.insetBy(dx: 0, dy: -additionalSafeAreaInsets.bottom) let intersection = safeAreaFrame.intersection(keyboardFrameInView) let animationDuration: TimeInterval = (notification.userInfo?[UIKeyboardAnimationDurationUserInfoKey] as? NSNumber)?.doubleValue ?? 0 let animationCurveRawNSN = notification.userInfo?[UIKeyboardAnimationCurveUserInfoKey] as? NSNumber let animationCurveRaw = animationCurveRawNSN?.uintValue ?? UIViewAnimationOptions.curveEaseInOut.rawValue let animationCurve = UIViewAnimationOptions(rawValue: animationCurveRaw) UIView.animate(withDuration: animationDuration, delay: 0, options: animationCurve, animations: { self.additionalSafeAreaInsets.bottom = intersection.height self.view.layoutIfNeeded() }, completion: nil) } }}
I use a different approach. I have a view (KeyboardProxyView) that I add to my view hierarchy.I pin this to the bottom of the main view, and adjust its height with the keyboard. This means we can treat the keyboardProxy as if it is the keyboard view - except that it is a normal view, so you can use constraints on it.
This allows me to constrain my other views relative to the keyboardProxy manually.
e.g. - my toolbar isn't constrained at all, but I might have inputField.bottom >= keyboardProxy.top
Code below(note - I use HSObserver and PureLayout for notifications and autolayout - but you could easily rewrite that code if you prefer to avoid them)
import Foundationimport UIKitimport PureLayoutimport HSObserver/// Keyboard Proxy view will mimic the height of the keyboard/// You can then constrain other views to move up when the KeyboardProxy expands using AutoLayoutclass KeyboardProxyView: UIView, HSHasObservers { weak var keyboardProxyHeight: NSLayoutConstraint! override func didMoveToSuperview() { keyboardProxyHeight = self.autoSetDimension(.height, toSize: 0) let names = [UIResponder.keyboardWillShowNotification,UIResponder.keyboardWillHideNotification] HSObserver.init(forNames: names) { [weak self](notif) in self?.updateKeyboardProxy(notification: notif) }.add(to: self) activateObservers() } var parentViewController: UIViewController? { var parentResponder: UIResponder? = self while parentResponder != nil { parentResponder = parentResponder!.next if let viewController = parentResponder as? UIViewController { return viewController } } return nil } func updateKeyboardProxy(notification:Notification){ let userInfo = notification.userInfo! let animationDuration = (userInfo[UIResponder.keyboardAnimationDurationUserInfoKey] as! NSNumber).doubleValue let keyboardEndFrame = (userInfo[UIResponder.keyboardFrameEndUserInfoKey] as! NSValue).cgRectValue let convertedKeyboardEndFrame = self.superview!.convert(keyboardEndFrame, from: self.window) let rawAnimationCurve = (notification.userInfo![UIResponder.keyboardAnimationCurveUserInfoKey] as! NSNumber).uint32Value << 16 let animationCurve = UIView.AnimationOptions(rawValue:UInt(rawAnimationCurve)) keyboardProxyHeight.constant = self.superview!.bounds.maxY - convertedKeyboardEndFrame.minY //keyboardProxyHeight.constant = keyboardEndFrame.height UIView.animate(withDuration: animationDuration, delay: 0.0, options: [.beginFromCurrentState, animationCurve], animations: { self.parentViewController?.view.layoutIfNeeded() }, completion: nil) }}