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