扩展 iOS 11 安全区以包括键盘
Extend iOS 11 Safe Area to include the keyboard
iOS11 中引入的新安全区域布局指南非常适合防止内容显示在栏下方,但不包括键盘。这意味着当显示键盘时,内容仍然隐藏在它后面,这就是我要解决的问题。
我的方法是基于监听键盘通知,然后通过additionalSafeAreaInsets
调整安全区域。
这是我的代码:
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();
}
}
}
这很有效,因为 MyController
是 UIViewController
和 UITableView
延伸穿过整个安全区域。现在当键盘出现时,底部被向上推,这样键盘后面就没有单元格了。
问题出在底栏上。我在底部还有一个工具栏,它已经包含在安全区域中。因此,将全键盘高度设置为额外的安全区域插入会将 table 视图的底部向上推太多,恰好是底部栏的高度。为了使此方法正常工作,我必须将 additionalSafeAreaInsets.bottom
设置为等于键盘高度减去底部栏的高度。
问题一:获得底部当前安全区缺口的最佳方法是什么?手动获取工具栏框架并使用其高度?还是直接从safe area layout guide中获取gap?
问题 2:据推测底部栏应该可以在不改变键盘大小的情况下改变大小,所以我还应该实现一些方法来监听栏框架的变化。这在 viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator)
中最好吗?还是其他地方?
谢谢
对我来说似乎有用的是计算 view.safeAreaLayoutGuide.layoutFrame
和键盘框架之间的交集,然后将其高度设置为 additionalSafeAreaInsets.bottom
,而不是整个键盘框架的高度.我的视图控制器中没有工具栏,但我有一个选项卡栏并且它被正确地解释了。
完整代码:
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)
}
}
如果您需要支持回到 IOS11 之前的版本,您可以
使用 Fabio 的函数并添加:
if #available(iOS 11.0, *) { }
最终解决方案:
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)
}
}
}
排除底部安全区对我有用:
NSValue keyboardBounds = (NSValue)notification.UserInfo.ObjectForKey(UIKeyboard.FrameEndUserInfoKey);
_bottomViewBottomConstraint.Constant = keyboardBounds.RectangleFValue.Height - UIApplication.SharedApplication.KeyWindow.SafeAreaInsets.Bottom;
View.LayoutIfNeeded();
我使用不同的方法。我有一个添加到我的视图层次结构中的视图 (KeyboardProxyView)。
我将它固定在主视图的底部,并用键盘调整它的高度。这意味着我们可以将 keyboardProxy 视为键盘视图 - 除了它是普通视图,因此您可以对其使用约束。
这使我可以手动限制与 keyboardProxy 相关的其他视图。
例如- 我的工具栏完全不受限制,但我可能有 inputField.bottom >= keyboardProxy.top
代码如下
(注意 - 我将 HSObserver 和 PureLayout 用于通知和自动布局 - 但如果你愿意避免使用它们,你可以轻松重写该代码)
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)
}
}
底部插图:
var safeAreaBottomInset: CGFloat = 0
if #available(iOS 11.0, *) {
safeAreaBottomInset = UIApplication.shared.keyWindow?.safeAreaInsets.bottom ?? 0
}
全键盘功能:
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
)
}
iOS11 中引入的新安全区域布局指南非常适合防止内容显示在栏下方,但不包括键盘。这意味着当显示键盘时,内容仍然隐藏在它后面,这就是我要解决的问题。
我的方法是基于监听键盘通知,然后通过additionalSafeAreaInsets
调整安全区域。
这是我的代码:
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();
}
}
}
这很有效,因为 MyController
是 UIViewController
和 UITableView
延伸穿过整个安全区域。现在当键盘出现时,底部被向上推,这样键盘后面就没有单元格了。
问题出在底栏上。我在底部还有一个工具栏,它已经包含在安全区域中。因此,将全键盘高度设置为额外的安全区域插入会将 table 视图的底部向上推太多,恰好是底部栏的高度。为了使此方法正常工作,我必须将 additionalSafeAreaInsets.bottom
设置为等于键盘高度减去底部栏的高度。
问题一:获得底部当前安全区缺口的最佳方法是什么?手动获取工具栏框架并使用其高度?还是直接从safe area layout guide中获取gap?
问题 2:据推测底部栏应该可以在不改变键盘大小的情况下改变大小,所以我还应该实现一些方法来监听栏框架的变化。这在 viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator)
中最好吗?还是其他地方?
谢谢
对我来说似乎有用的是计算 view.safeAreaLayoutGuide.layoutFrame
和键盘框架之间的交集,然后将其高度设置为 additionalSafeAreaInsets.bottom
,而不是整个键盘框架的高度.我的视图控制器中没有工具栏,但我有一个选项卡栏并且它被正确地解释了。
完整代码:
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)
}
}
如果您需要支持回到 IOS11 之前的版本,您可以 使用 Fabio 的函数并添加:
if #available(iOS 11.0, *) { }
最终解决方案:
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)
}
}
}
排除底部安全区对我有用:
NSValue keyboardBounds = (NSValue)notification.UserInfo.ObjectForKey(UIKeyboard.FrameEndUserInfoKey);
_bottomViewBottomConstraint.Constant = keyboardBounds.RectangleFValue.Height - UIApplication.SharedApplication.KeyWindow.SafeAreaInsets.Bottom;
View.LayoutIfNeeded();
我使用不同的方法。我有一个添加到我的视图层次结构中的视图 (KeyboardProxyView)。 我将它固定在主视图的底部,并用键盘调整它的高度。这意味着我们可以将 keyboardProxy 视为键盘视图 - 除了它是普通视图,因此您可以对其使用约束。
这使我可以手动限制与 keyboardProxy 相关的其他视图。
例如- 我的工具栏完全不受限制,但我可能有 inputField.bottom >= keyboardProxy.top
代码如下 (注意 - 我将 HSObserver 和 PureLayout 用于通知和自动布局 - 但如果你愿意避免使用它们,你可以轻松重写该代码)
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)
}
}
底部插图:
var safeAreaBottomInset: CGFloat = 0
if #available(iOS 11.0, *) {
safeAreaBottomInset = UIApplication.shared.keyWindow?.safeAreaInsets.bottom ?? 0
}
全键盘功能:
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
)
}