如何使用 iOS 上的约束将视图附加到键盘上边缘

How can I attach view to keyboard top edge using constraints on iOS

我在视图控制器的底部附加了一个简单的视图:

let view = UIView()
view.backgroundColor = .gray
view.translatesAutoresizingMaskIntoConstraints = false
let viewBottomPositionConstraint = view.bottomAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.bottomAnchor)
NSLayoutConstraint.activate([
    view.heightAnchor.constraint(equalToConstant: 100),
    view.leftAnchor.constraint(equalTo: self.view.leftAnchor),
    view.rightAnchor.constraint(equalTo: self.view.rightAnchor),
    viewBottomPositionConstraint
])

当键盘 appears/disappears 跟随键盘动画

时,我需要那个视图上下移动

我已经创建了KeyboardNotifier,用法很简单:

var keyboardNotifier: KeyboardNotifier!

override func viewDidLoad() {
    super.viewDidLoad()
    
    keyboardNotifier = KeyboardNotifier(parentView: view, constraint: viewBottomPositionConstraint)
}

override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)
    keyboardNotifier.enabled = true

override func viewDidDisappear(_ animated: Bool) {
    keyboardNotifier.enabled = false
    super.viewDidDisappear(animated)
}

viewWillAppear/viewDidDisappear 中应该是 enabled/disabled 以防止在其他屏幕上进行交互。

通过侦听 keyboardWillChangeFrameNotification

更新约束值

我不得不将它从键盘创建的动画中移出以防止出现故障,并在我自己的动画中并行更新约束。

final class KeyboardNotifier {
    var enabled: Bool = true {
        didSet {
            setNeedsUpdateConstraint()
        }
    }
    
    init(
        parentView: UIView,
        constraint: NSLayoutConstraint
    ) {
        self.parentView = parentView
        self.constraint = constraint
        
        baseConstant = constraint.constant
        notificationObserver = NotificationCenter.default
            .addObserver(
                forName: UIResponder.keyboardWillChangeFrameNotification,
                object: nil,
                queue: .main
            ) { [weak self] in
                self?.keyboardEndFrame = ([=11=].userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue
                self?.setNeedsUpdateConstraint(animationDuration: UIView.inheritedAnimationDuration)
            }
    }
    
    private weak var parentView: UIView?
    private weak var constraint: NSLayoutConstraint?
    private let baseConstant: CGFloat
    private var notificationObserver: NSObjectProtocol!
    private var keyboardEndFrame: CGRect?
    private var latestAnimationDuration: TimeInterval?
    
    private func setNeedsUpdateConstraint(animationDuration: TimeInterval = 0) {
        guard
            latestAnimationDuration == nil
            || animationDuration > latestAnimationDuration!
            else { return }
        let shouldUpdate = latestAnimationDuration == nil
        latestAnimationDuration = animationDuration
        if shouldUpdate {
            DispatchQueue.main.async {
                self.updateConstraint()
            }
        }
    }
    
    private func updateConstraint() {
        defer {
            latestAnimationDuration = nil
        }
        guard
            let latestAnimationDuration = latestAnimationDuration,
            enabled,
            let keyboardEndFrame = keyboardEndFrame,
            let parentView = parentView,
            let constraint = constraint
            else { return }
        
        UIView.performWithoutAnimation {
            parentView.layoutIfNeeded()
        }
        let isParentFirstItem = constraint.firstItem is UILayoutGuide || constraint.firstItem === parentView
        let followsLayoutGuide = constraint.firstItem is UILayoutGuide || constraint.secondItem is UILayoutGuide
        let multiplierSign: CGFloat = isParentFirstItem ? 1 : -1
        let screenHeight = UIScreen.main.bounds.height
        if keyboardEndFrame.minY >= screenHeight {
            constraint.constant = baseConstant
        } else {
            let safeAreaInsets = (followsLayoutGuide ? parentView.safeAreaInsets.bottom : 0)
            // if our constraint makes view invisible when keyboard is hidden, we need to ignore it
            let fixedBaseConstant = max(multiplierSign * baseConstant, 0)
            constraint.constant = multiplierSign * (screenHeight - keyboardEndFrame.minY - safeAreaInsets + fixedBaseConstant)
        }
        
        UIView.animate(
            withDuration: latestAnimationDuration,
            delay: 0,
            options: .beginFromCurrentState,
            animations: { parentView.layoutIfNeeded() },
            completion: nil
        )
    }
}