106 lines
3.7 KiB
Swift
106 lines
3.7 KiB
Swift
//
|
||
// KeyboardAvoidingDialogView.swift
|
||
// suixinkan
|
||
//
|
||
|
||
import SnapKit
|
||
import UIKit
|
||
|
||
/// 自定义居中弹窗的键盘避让基类,负责在键盘弹出时同步移动内容卡片。
|
||
class KeyboardAvoidingDialogView: UIView {
|
||
|
||
/// 需要避让键盘的弹窗内容视图,通常是白色卡片容器。
|
||
var keyboardAvoidingContentView: UIView?
|
||
|
||
/// 内容视图相对父视图的 centerY 约束,用于随键盘动画更新偏移。
|
||
var keyboardAvoidingCenterYConstraint: Constraint?
|
||
|
||
/// 内容视图与屏幕顶部、键盘顶部之间保留的最小距离。
|
||
var keyboardAvoidingMinimumSpacing: CGFloat = AppSpacing.md
|
||
|
||
private var keyboardObservers: [NSObjectProtocol] = []
|
||
|
||
override init(frame: CGRect) {
|
||
super.init(frame: frame)
|
||
}
|
||
|
||
@available(*, unavailable)
|
||
required init?(coder: NSCoder) {
|
||
fatalError("init(coder:) has not been implemented")
|
||
}
|
||
|
||
deinit {
|
||
unregisterKeyboardObservers()
|
||
}
|
||
|
||
override func didMoveToSuperview() {
|
||
super.didMoveToSuperview()
|
||
if superview == nil {
|
||
unregisterKeyboardObservers()
|
||
} else {
|
||
registerKeyboardObservers()
|
||
}
|
||
}
|
||
|
||
private func registerKeyboardObservers() {
|
||
unregisterKeyboardObservers()
|
||
let center = NotificationCenter.default
|
||
keyboardObservers = [
|
||
center.addObserver(
|
||
forName: UIResponder.keyboardWillChangeFrameNotification,
|
||
object: nil,
|
||
queue: .main
|
||
) { [weak self] notification in
|
||
self?.handleKeyboardNotification(notification)
|
||
},
|
||
center.addObserver(
|
||
forName: UIResponder.keyboardWillHideNotification,
|
||
object: nil,
|
||
queue: .main
|
||
) { [weak self] notification in
|
||
self?.handleKeyboardNotification(notification)
|
||
}
|
||
]
|
||
}
|
||
|
||
private func unregisterKeyboardObservers() {
|
||
keyboardObservers.forEach(NotificationCenter.default.removeObserver)
|
||
keyboardObservers.removeAll()
|
||
}
|
||
|
||
private func handleKeyboardNotification(_ notification: Notification) {
|
||
layoutIfNeeded()
|
||
guard let userInfo = notification.userInfo else { return }
|
||
|
||
let duration = (userInfo[UIResponder.keyboardAnimationDurationUserInfoKey] as? NSNumber)?.doubleValue ?? 0.25
|
||
let curveRaw = (userInfo[UIResponder.keyboardAnimationCurveUserInfoKey] as? NSNumber)?.uintValue
|
||
?? UInt(UIView.AnimationCurve.easeInOut.rawValue)
|
||
let options = UIView.AnimationOptions(rawValue: curveRaw << 16)
|
||
|
||
keyboardAvoidingCenterYConstraint?.update(offset: keyboardAvoidanceOffset(from: userInfo))
|
||
UIView.animate(withDuration: duration, delay: 0, options: options) {
|
||
self.layoutIfNeeded()
|
||
}
|
||
}
|
||
|
||
private func keyboardAvoidanceOffset(from userInfo: [AnyHashable: Any]) -> CGFloat {
|
||
guard
|
||
let contentView = keyboardAvoidingContentView,
|
||
let keyboardFrameValue = userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue
|
||
else {
|
||
return 0
|
||
}
|
||
|
||
let keyboardFrame = convert(keyboardFrameValue.cgRectValue, from: nil)
|
||
guard keyboardFrame.minY < bounds.height else { return 0 }
|
||
|
||
let contentHeight = contentView.bounds.height
|
||
let baseMinY = bounds.midY - contentHeight / 2
|
||
let baseMaxY = bounds.midY + contentHeight / 2
|
||
let desiredBottom = keyboardFrame.minY - keyboardAvoidingMinimumSpacing
|
||
let neededShift = max(0, baseMaxY - desiredBottom)
|
||
let maxShift = max(0, baseMinY - keyboardAvoidingMinimumSpacing)
|
||
return -min(neededShift, maxShift)
|
||
}
|
||
}
|