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.
162 lines
5.1 KiB
Swift
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
|
|
}
|
|
}
|