Signal-iOS/SignalUI/Views/CustomKeyboard.swift
Adam Sharp ce9e1594ec
Fix attachment/media keyboard not appearing
Scaling the system keyboard height eagerly meant that if the window was not
set, the keyboard height would be set to 0.  Defer scaling the system keyboard
height until layout time when the view has been added to a view hierarchy.
2024-07-09 12:17:37 -04:00

162 lines
5.1 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()
NotificationCenter.default.addObserver(
self,
selector: #selector(orientationDidChange),
name: UIDevice.orientationDidChangeNotification,
object: UIDevice.current
)
translatesAutoresizingMaskIntoConstraints = false
allowsSelfSizing = true
resizeToSystemKeyboard()
}
required public init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
open func willPresent() {}
open func wasPresented() {}
open func wasDismissed() {}
public func registerWithView(_ view: UIView) {
view.addSubview(responder)
}
private lazy var responder = CustomKeyboardResponder(customKeyboard: self)
override open var isFirstResponder: Bool { return responder.isFirstResponder }
open override func becomeFirstResponder() -> Bool {
let result = responder.becomeFirstResponder()
if result { willPresent() }
return result
}
open override func resignFirstResponder() -> Bool {
return responder.resignFirstResponder()
}
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
private lazy var heightConstraint = autoSetDimension(.height, toSize: 0)
private struct SystemKeyboardHeight {
var landscape: CGFloat?
var portrait: CGFloat?
}
private var cachedSystemKeyboardHeight = SystemKeyboardHeight()
private var currentSystemKeyboardHeight: CGFloat? {
get {
let interfaceOrientation = window?.windowScene?.interfaceOrientation ?? .unknown
return interfaceOrientation.isLandscape ? cachedSystemKeyboardHeight.landscape : cachedSystemKeyboardHeight.portrait
}
set {
let interfaceOrientation = window?.windowScene?.interfaceOrientation ?? .unknown
let orientationKey = interfaceOrientation.isLandscape ? \SystemKeyboardHeight.landscape : \.portrait
cachedSystemKeyboardHeight[keyPath: orientationKey] = newValue
}
}
public func updateSystemKeyboardHeight(_ height: CGFloat) {
// Only respect this height if it's reasonable, we don't want
// to have a tiny keyboard.
guard height > 170 else { return }
currentSystemKeyboardHeight = height
resizeToSystemKeyboard()
}
open func resizeToSystemKeyboard() {
guard var keyboardHeight = currentSystemKeyboardHeight 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
}
if let window {
// App frame height changes based on orientation (i.e. its the smaller dimension when landscape).
// Cap the height for custom keyboard because our layout breaks if we extend too tall.
let maxHeight = window.frame.height * 0.75
if keyboardHeight > maxHeight {
keyboardHeight = maxHeight
}
}
// 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 = keyboardHeight
}
open override func layoutSubviews() {
super.layoutSubviews()
resizeToSystemKeyboard()
}
@objc
open func orientationDidChange() {
resizeToSystemKeyboard()
}
}
private class CustomKeyboardResponder: UITextView {
public weak var customKeyboard: CustomKeyboard?
init(customKeyboard: CustomKeyboard) {
self.customKeyboard = customKeyboard
super.init(frame: .zero, textContainer: nil)
autocorrectionType = .no
keyboardAppearance = Theme.keyboardAppearance
inputAssistantItem.leadingBarButtonGroups = []
inputAssistantItem.trailingBarButtonGroups = []
}
override var inputView: UIView? {
get { customKeyboard }
set {}
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public override var canBecomeFirstResponder: Bool {
return true
}
}