将iOS 11安全区域扩展至包括键盘

20

iOS 11引入的新的安全区域布局指南非常适用于防止内容显示在栏下,但它不包括键盘。这意味着当键盘显示时,内容仍然隐藏在其后面,这是我要解决的问题。

我的方法是基于监听键盘通知,然后通过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 沿着整个安全区域延伸。现在当键盘出现时,底部会被推上去,以便没有单元格位于键盘后面。

问题是底部栏的位置。我在底部也有一个工具栏,已包含在安全区域内。因此,将完整键盘高度设置为额外的安全区域插入会将表视图的底部过多地向上推动,正好等于底部栏的高度。要使此方法有效,必须将 additionalSafeAreaInsets.bottom 设置为键盘高度减去底部栏的高度。

问题1:获取当前底部安全区域间隙的最佳方法是什么?手动获取工具条的框架并使用其高度?还是直接从安全区域布局指南中获取间隙?

问题2:可以假定底部栏可以改变大小而不影响键盘的大小,因此我也应该实现某种监听栏框架更改的方法。最好在 viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) 中完成吗?或其他地方?

谢谢


我认为这也很有趣。https://github.com/IdleHandsApps/IHKeyboardAvoiding - twodayslate
使用UITableViewController,它内置了键盘避让功能以及其他一些有用的功能。 - malhal
5个回答

40

对我有效的方法是计算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)
    }
}

1
这是一个更好的解决方案来处理iPhoneX,因为它的高度包括安全区域以外的区域。 - Peter Johnson
你在使用iPhone X时有没有遇到任何问题?当键盘在iPhone X上显示时,其Y轴位置似乎比实际屏幕高了一些。 - Jonny
我有一个布局,需要在键盘出现时将UI元素放置在其正上方。具体来说,它是一个uitextfield,并且我实际上没有像您的情况那样使用任何工具栏等,但我正在使用相同的获取框架的方式(即end key)。对于iPhone X,我得到的结束位置大约比实际位置高20个点左右。 - Jonny
发现我的问题,详见:https://stackoverflow.com/a/47069060/129202 - Jonny
1
提示:如果您正在使用覆盖textRecteditingRectUITextField子类,请将self.view.layoutIfNeeded()包装在UIView.setAnimationsEnabled(false)UIView.setAnimationsEnabled(true)中,以防止在切换字段时文本跳动。 - thattyson
显示剩余4条评论

6
如果您需要支持早期的iOS 11版本,您可以使用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)
        }
    }
}

1
底部插图:
    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
        )
    }

1
我采用不同的方法。我有一个视图(KeyboardProxyView),将其添加到我的视图层次结构中。我将其固定在主视图底部,并根据键盘调整其高度。这意味着我们可以像处理键盘视图一样来处理键盘代理-除了它是一个普通视图,所以您可以在其上使用约束。
这使我能够手动相对于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)
    }
}

0

对我来说,排除底部安全区域是有效的:

 NSValue keyboardBounds = (NSValue)notification.UserInfo.ObjectForKey(UIKeyboard.FrameEndUserInfoKey);

_bottomViewBottomConstraint.Constant = keyboardBounds.RectangleFValue.Height - UIApplication.SharedApplication.KeyWindow.SafeAreaInsets.Bottom;
                        View.LayoutIfNeeded();

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接