Signal-iOS/SignalUI/Views/CustomKeyboard.swift
Igor Solomennikov 164f6e8033
Re-worked custom chat keyboard size calculations.
• make sticker / attachment keyboard follow system (text) keyboard height as closely as possible.
• fix chat input bar obscured by keyboard after interface rotation.
2025-09-30 14:42:15 -07:00

131 lines
4.4 KiB
Swift

//
// Copyright 2019 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import SignalServiceKit
open class CustomKeyboard: UIInputView {
public let contentView = UIView()
public init() {
super.init(frame: .zero, inputViewStyle: .default)
addSubview(contentView)
contentView.autoPinEdgesToSuperviewEdges()
translatesAutoresizingMaskIntoConstraints = false
allowsSelfSizing = false
}
required public init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
open func willPresent() {}
open func wasPresented() {}
open func willDismiss() {}
open func wasDismissed() {}
open override func willMove(toSuperview newSuperview: UIView?) {
DispatchQueue.main.async { [weak self] in
guard let self else { return }
if newSuperview != nil {
self.willPresent()
} else {
self.willDismiss()
}
}
}
open override func didMoveToSuperview() {
// Call wasPresented/wasDismissed on the next run loop,
// once this view hierarchy change has finished.
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
if self.superview == nil {
self.wasDismissed()
} else {
self.wasPresented()
}
}
}
// MARK: - Height Management
public func setSystemKeyboardHeight(
_ height: CGFloat,
forTraitCollection traitCollection: UITraitCollection
) {
// Only respect this height if it's reasonable, we don't want
// to have a tiny keyboard.
guard height > 170 else { return }
let key = SizeClassKey(horizontal: traitCollection.horizontalSizeClass,
vertical: traitCollection.verticalSizeClass)
if CustomKeyboard.cachedKeyboardHeights[key] == nil {
CustomKeyboard.cachedKeyboardHeights[key] = height
}
updateHeightConstraint()
}
public class func hasCachedHeight(forTraitCollection traitCollection: UITraitCollection) -> Bool {
let key = SizeClassKey(horizontal: traitCollection.horizontalSizeClass,
vertical: traitCollection.verticalSizeClass)
return CustomKeyboard.cachedKeyboardHeights[key] != nil
}
private lazy var heightConstraint = heightAnchor.constraint(equalToConstant: 0)
private struct SizeClassKey: Hashable {
let horizontal: UIUserInterfaceSizeClass
let vertical: UIUserInterfaceSizeClass
func hash(into hasher: inout Hasher) {
hasher.combine(horizontal.rawValue)
hasher.combine(vertical.rawValue)
}
static func == (lhs: SizeClassKey, rhs: SizeClassKey) -> Bool {
lhs.horizontal == rhs.horizontal && lhs.vertical == rhs.vertical
}
}
private static var cachedKeyboardHeights = [SizeClassKey: CGFloat]()
private func updateHeightConstraint() {
let key = SizeClassKey(horizontal: traitCollection.horizontalSizeClass,
vertical: traitCollection.verticalSizeClass)
guard let cachedHeight = CustomKeyboard.cachedKeyboardHeights[key] else {
// We don't have a cached height for this orientation,
// let the auto sizing do its best guess at what the
// system keyboard height might be.
heightConstraint.isActive = false
allowsSelfSizing = false
return
}
// We have a cached height so we want to size ourself. The system
// sizing isn't a 100% match to the system keyboard's size and
// does not account for things like the quicktype toolbar.
allowsSelfSizing = true
heightConstraint.isActive = true
heightConstraint.constant = cachedHeight
}
open override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)
// We only care about changes in size classes, which would be triggered by interface rotation.
guard
previousTraitCollection?.horizontalSizeClass != traitCollection.horizontalSizeClass ||
previousTraitCollection?.verticalSizeClass != traitCollection.verticalSizeClass else
{
return
}
updateHeightConstraint()
}
}