The new Safe Area layout guide introduced in iOS 11 works great to prevent content from displaying below bars, but it excludes the keyboard. That means that when a keyboard is displayed, content is still hidden behind it and this is the problem I am trying to solve.
My approach is based on listening to keyboard notifications and then adjusting the safe area through additionalSafeAreaInsets
.
Here is my code:
override func viewDidLoad() {
let notificationCenter = NotificationCenter.default
notificationCenter.addObserver(self, selector: #selector(keyboardWillShow(notification:)), name: NSNotification.Name.UIKeyboardWillShow, object: nil)
notificationCenter.addObserver(self, selector: #selector(keyboardWillHide(notification:)), name: NSNotification.Name.UIKeyboardWillHide, object: nil)
notificationCenter.addObserver(self, selector: #selector(keyboardWillChange(notification:)), name: NSNotification.Name.UIKeyboardWillChangeFrame, object: nil)
}
//MARK: - Keyboard
extension MyViewController {
@objc func keyboardWillShow(notification: NSNotification) {
let userInfo = notification.userInfo!
let keyboardHeight = (userInfo[UIKeyboardFrameEndUserInfoKey] as! NSValue).cgRectValue.height
additionalSafeAreaInsets = UIEdgeInsets(top: 0, left: 0, bottom: keyboardHeight, right: 0)
UIView.animate(withDuration: 0.3) {
self.view.layoutIfNeeded();
}
}
@objc func keyboardWillHide(notification: NSNotification) {
additionalSafeAreaInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
UIView.animate(withDuration: 0.3) {
self.view.layoutIfNeeded();
}
}
@objc func keyboardWillChange(notification: NSNotification) {
let userInfo = notification.userInfo!
let keyboardHeight = (userInfo[UIKeyboardFrameEndUserInfoKey] as! NSValue).cgRectValue.height
additionalSafeAreaInsets = UIEdgeInsets(top: 0, left: 0, bottom: keyboardHeight, right: 0)
UIView.animate(withDuration: 0.3) {
self.view.layoutIfNeeded();
}
}
}
This works well as the MyController
is a UIViewController
with a UITableView
that extends through the whole safe area. Now when the keyboard appears, the bottom is pushed up so that no cells are behind the keyboard.
The problem is with bottom bars. I also have a toolbar at the bottom which is already included in the safe area. Therefore, setting full keyboard height as additional safe area inset pushes the bottom of the table view up too much by exactly the height of the bottom bar. For this method to work well, I must set the additionalSafeAreaInsets.bottom
to be equal to the keyboard height minus the height of the bottom bar.
Question 1: What is the best way to get the current safe area gap on the bottom? Manually get frame of toolbar and use its height? Or is it possible to get the gap directly from the safe area layout guide?
Question 2: Presumably it should be possible for the bottom bar to change size without the keyboard changing size so I should also implement some method listening to change in frame of the bar. Is this best done in viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator)
? Or elsewhere?
Thank you
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 = 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)
}
}