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 UIKit
public 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 = userInfo[UIResponder.keyboardAnimationDurationUserInfoKey]
let animationDuration: TimeInterval = (keyboardAnimationDuration as? NSNumber)?.doubleValue ?? 0
let animationCurveRawNSN = 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 Foundation
import UIKit
import PureLayout
import 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 AutoLayout
class 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)
}
}
bottom inset:
var safeAreaBottomInset: CGFloat = 0
if #available(iOS 11.0, *) {
safeAreaBottomInset = UIApplication.shared.keyWindow?.safeAreaInsets.bottom ?? 0
}
whole keyboard function:
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
NotificationCenter.default.addObserver(self, selector: #selector(self.onKeyboardWillChangeFrame), name: UIResponder.keyboardWillChangeFrameNotification, object: nil)
}
@objc private func onKeyboardWillChangeFrame(_ notification: NSNotification) {
guard let window = self.view.window,
let info = notification.userInfo,
let keyboardFrame = info[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect,
let duration = info[UIResponder.keyboardAnimationDurationUserInfoKey] as? TimeInterval,
let animationCurve = info[UIResponder.keyboardAnimationCurveUserInfoKey] as? NSNumber
else {
return
}
var safeAreaInset: CGFloat = 0
if #available(iOS 11.0, *) {
safeAreaInset = UIApplication.shared.keyWindow?.safeAreaInsets.bottom ?? 0
}
self.keyboardConstraint.constant = max(0, self.view.frame.height - window.convert(keyboardFrame, to: self.view).minY - safeAreaInset)
UIView.animate(
withDuration: duration,
delay: 0,
options: [UIView.AnimationOptions(rawValue: animationCurve.uintValue), .beginFromCurrentState],
animations: { self.view.layoutIfNeeded() },
completion: nil
)
}