Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
signalapp
GitHub Repository: signalapp/Signal-iOS
Path: blob/main/SignalUI/Views/CustomKeyboard.swift
1 views
//
// 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
    }

    public required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    open func willPresent() {}
    open func wasPresented() {}
    open func willDismiss() {}
    open func wasDismissed() {}

    override open func willMove(toSuperview newSuperview: UIView?) {
        if newSuperview != nil {
            self.willPresent()
        } else {
            self.willDismiss()
        }
    }

    override open 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 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
        }
    }

    public func updateHeightForPresentation() {
        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)
        }

    }

    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
    }

    override open 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()
    }
}