Goal

我想制作一个只在我的应用程序中使用的自定义键盘,而不是需要安装的系统键盘.

What I have read and tried

Documentation

上述第一条规定:

确保定制的全系统键盘确实是你想要的

这就是我读到上面第二篇文章的原因.然而,那篇文章没有足够的细节让我开始.

Tutorials

我从上面列表中的第二个教程中获得了一个可用的键盘.然而,我找不到任何教程来展示如何制作Custom Views for Data Input文档中描述的仅应用程序内的键盘.

Stack Overflow

在回答当前问题的过程中,我也问了(并回答了)这些问题.

Question

有没有人有一个应用内定制键盘的最小示例(甚至只有一个按钮)?我不是在寻找一个完整的教程,只是一个概念证明,我可以扩展自己.

推荐答案

关键是使用现有的UIKeyInput协议,UITextField已经符合该协议.然后,键盘视图只需将insertText()deleteBackward()发送到控件.

以下示例创建自定义数字键盘:

class DigitButton: UIButton {
    var digit: Int = 0
}

class NumericKeyboard: UIView {
    weak var target: (UIKeyInput & UITextInput)?
    var useDecimalSeparator: Bool

    var numericButtons: [DigitButton] = (0...9).map {
        let button = DigitButton(type: .system)
        button.digit = $0
        button.setTitle("\($0)", for: .normal)
        button.titleLabel?.font = .preferredFont(forTextStyle: .largeTitle)
        button.setTitleColor(.black, for: .normal)
        button.layer.borderWidth = 0.5
        button.layer.borderColor = UIColor.darkGray.cgColor
        button.accessibilityTraits = [.keyboardKey]
        button.addTarget(self, action: #selector(didTapDigitButton(_:)), for: .touchUpInside)
        return button
    }

    var deleteButton: UIButton = {
        let button = UIButton(type: .system)
        button.setTitle("⌫", for: .normal)
        button.titleLabel?.font = .preferredFont(forTextStyle: .largeTitle)
        button.setTitleColor(.black, for: .normal)
        button.layer.borderWidth = 0.5
        button.layer.borderColor = UIColor.darkGray.cgColor
        button.accessibilityTraits = [.keyboardKey]
        button.accessibilityLabel = "Delete"
        button.addTarget(self, action: #selector(didTapDeleteButton(_:)), for: .touchUpInside)
        return button
    }()

    lazy var decimalButton: UIButton = {
        let button = UIButton(type: .system)
        let decimalSeparator = Locale.current.decimalSeparator ?? "."
        button.setTitle(decimalSeparator, for: .normal)
        button.titleLabel?.font = .preferredFont(forTextStyle: .largeTitle)
        button.setTitleColor(.black, for: .normal)
        button.layer.borderWidth = 0.5
        button.layer.borderColor = UIColor.darkGray.cgColor
        button.accessibilityTraits = [.keyboardKey]
        button.accessibilityLabel = decimalSeparator
        button.addTarget(self, action: #selector(didTapDecimalButton(_:)), for: .touchUpInside)
        return button
    }()

    init(target: UIKeyInput & UITextInput, useDecimalSeparator: Bool = false) {
        self.target = target
        self.useDecimalSeparator = useDecimalSeparator
        super.init(frame: .zero)
        configure()
    }

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

// MARK: - Actions

extension NumericKeyboard {
    @objc func didTapDigitButton(_ sender: DigitButton) {
        insertText("\(sender.digit)")
    }

    @objc func didTapDecimalButton(_ sender: DigitButton) {
        insertText(Locale.current.decimalSeparator ?? ".")
    }

    @objc func didTapDeleteButton(_ sender: DigitButton) {
        target?.deleteBackward()
    }
}

// MARK: - Private initial configuration methods

private extension NumericKeyboard {
    func configure() {
        autoresizingMask = [.flexibleWidth, .flexibleHeight]
        addButtons()
    }

    func addButtons() {
        let stackView = createStackView(axis: .vertical)
        stackView.frame = bounds
        stackView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        addSubview(stackView)

        for row in 0 ..< 3 {
            let subStackView = createStackView(axis: .horizontal)
            stackView.addArrangedSubview(subStackView)

            for column in 0 ..< 3 {
                subStackView.addArrangedSubview(numericButtons[row * 3 + column + 1])
            }
        }

        let subStackView = createStackView(axis: .horizontal)
        stackView.addArrangedSubview(subStackView)

        if useDecimalSeparator {
            subStackView.addArrangedSubview(decimalButton)
        } else {
            let blank = UIView()
            blank.layer.borderWidth = 0.5
            blank.layer.borderColor = UIColor.darkGray.cgColor
            subStackView.addArrangedSubview(blank)
        }

        subStackView.addArrangedSubview(numericButtons[0])
        subStackView.addArrangedSubview(deleteButton)
    }

    func createStackView(axis: NSLayoutConstraint.Axis) -> UIStackView {
        let stackView = UIStackView()
        stackView.axis = axis
        stackView.alignment = .fill
        stackView.distribution = .fillEqually
        return stackView
    }

    func insertText(_ string: String) {
        guard let range = target?.selectedRange else { return }

        if let textField = target as? UITextField, textField.delegate?.textField?(textField, shouldChangeCharactersIn: range, replacementString: string) == false {
            return
        }

        if let textView = target as? UITextView, textView.delegate?.textView?(textView, shouldChangeTextIn: range, replacementText: string) == false {
            return
        }

        target?.insertText(string)
    }
}

// MARK: - UITextInput extension

extension UITextInput {
    var selectedRange: NSRange? {
        guard let textRange = selectedTextRange else { return nil }

        let location = offset(from: beginningOfDocument, to: textRange.start)
        let length = offset(from: textRange.start, to: textRange.end)
        return NSRange(location: location, length: length)
    }
}

然后你可以:

textField.inputView = NumericKeyboard(target: textField)

这就产生了:

enter image description here

或者,如果您也需要十进制分隔符,可以:

textField.inputView = NumericKeyboard(target: textField, useDecimalSeparator: true)

上面的内容相当简单,但它说明了这个 idea :创建自己的输入视图,并使用UIKeyInput协议将键盘输入传递给控件.

另外请注意使用accessibilityTraits来获得正确的"口语内容"»"口语屏幕"行为.如果你的按钮使用图像,也要确保设置accessibilityLabel.

Swift相关问答推荐

如何获取SCNCamera正在查看的网格点(SCNNode)的局部坐标和局部法线?

阻塞(受CPU限制的)任务的异步功能?

从SwiftUI使用UIPageView时,同一页面连续显示两次的问题

在SwiftUI中,我如何确保带有符号的单词不会被拆分为两行?

有条件地在同一视图上设置两个不同的过渡

为什么我不能在这个 Swift 间接枚举中返回 self ?

我如何在 swift 中的 viewdidload 之前进行委托?

SwiftUI .task 视图修改器:运行在哪个线程中?

仅当 boolean 为真时才实现方法(application:didReceiveRemoteNotification)

macOS 的窗口框架中使用的真实类型和真实类型是什么?

在 SwiftUI 中将属性显式设置为其默认值

在 SwiftUI 中操作绑定变量

SwiftUI Grid 中的数组旋转

如何在 SwiftUI 的 fileImporter 中为 allowedContentTypes 设置 xcodeproj 类型?

Xcode 13.3 将函数调用更改为属性访问

如何在 Xcode 中的 UITests 下以通用方式访问后退栏按钮项?

如何快速判断设备方向是横向还是横向?

CMutablePointer - 如何访问它?

Void 函数中意外的非 Void 返回值 (Swift 2.0)

判断用户是否登录到 iCloud?Swift /iOS