自定义视图用于数据输入的Swift示例(自定义应用程序键盘)

60

目标

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

我所阅读和尝试的内容

文档

上面的第一篇文章指出:

确保您要开发的是自定义的系统范围键盘。如果要为您的应用程序提供完全自定义的键盘或者要在应用程序中增加自定义按键来补充系统键盘,则 iOS SDK 提供了其他更好的选择。有关数据输入的自定义视图和输入附件视图,请参阅 iOS 文本编程指南中的 Custom Views for Data Input。

这就是我转到上面的第二篇文章的原因。然而,该文章并没有足够的细节让我开始实践。

教程

我能够从上述列表中的第二个教程中获得一个可用的键盘。但是,我找不到任何教程来演示如何制作应用内键盘,就像自定义数据输入视图文档中所描述的那样。

Stack Overflow

在回答当前问题的过程中,我还提出(并回答)了以下问题:

问题

有人有一个最小的例子(甚至只有一个按钮),可以演示如何制作应用内自定义键盘吗?我不是在寻找整个教程,只是想要一个可以扩展的概念证明。


你需要在多少个地方使用这个键盘输入?如果只有一个地方并且视图中没有太多的东西,你可以将键盘作为视图的一部分。 - Arc676
最终我想做一个有20或30个按钮的键盘。不过,首先我想制作一个非常简单的键盘,只有一个或两个按钮,以便学习过程。 - Suragch
3个回答

81

输入图像描述

这是一个基本的应用内键盘,相同的方法可以用来制作几乎任何键盘布局。以下是需要完成的主要任务:

  • 在.xib文件中创建键盘布局,其所有者是包含UIView子类的.swift文件。
  • 告诉UITextField使用自定义键盘。
  • 使用委托在键盘和主视图控制器之间进行通信。

创建.xib键盘布局文件

  • 在Xcode中转到文件 > 新建 > 文件... > iOS > 用户界面 > 视图以创建.xib文件。
  • 我称之为Keyboard.xib。
  • 添加所需的按钮。
  • 使用自动布局约束,以便无论键盘大小如何,按钮都会相应地调整大小。
  • 将文件的所有者(而不是根视图)设置为Keyboard.swift文件。这是常见的错误来源。请参阅末尾的注释。

创建.swift UIView子类键盘文件

  • 在Xcode中转到文件 > 新建 > 文件... > iOS > Source > Cocoa Touch类以创建.swift文件。
  • 我称之为Keyboard.swift。
  • 添加以下代码:

    import UIKit
    
    // The view controller will adopt this protocol (delegate)
    // and thus must contain the keyWasTapped method
    protocol KeyboardDelegate: class {
        func keyWasTapped(character: String)
    }
    
    class Keyboard: UIView {
    
        // This variable will be set as the view controller so that 
        // the keyboard can send messages to the view controller.
        weak var delegate: KeyboardDelegate?
    
        // MARK:- keyboard initialization
    
        required init?(coder aDecoder: NSCoder) {
            super.init(coder: aDecoder)
            initializeSubviews()
        }
    
        override init(frame: CGRect) {
            super.init(frame: frame)
            initializeSubviews()
        }
    
        func initializeSubviews() {
            let xibFileName = "Keyboard" // xib extention not included
            let view = Bundle.main.loadNibNamed(xibFileName, owner: self, options: nil)![0] as! UIView
            self.addSubview(view)
            view.frame = self.bounds
        }
    
        // MARK:- Button actions from .xib file
    
        @IBAction func keyTapped(sender: UIButton) {
            // When a button is tapped, send that information to the 
            // delegate (ie, the view controller)
            self.delegate?.keyWasTapped(character: sender.titleLabel!.text!) // could alternatively send a tag value
        }
    
    }
    
  • 从.xib文件中的按钮到.swift文件中的@IBAction方法进行控制拖动,以将它们全部连接起来。

  • 请注意协议和委托代码。有关委托工作原理的简单解释,请参见此答案

设置视图控制器

  • 向主Storyboard添加UITextField并使用IBOutlet将其连接到您的视图控制器。将其命名为textField
  • 使用以下代码设置视图控制器:

    import UIKit
    
    class ViewController: UIViewController, KeyboardDelegate {
    
        @IBOutlet weak var textField: UITextField!
    
        override func viewDidLoad() {
            super.viewDidLoad()
    
            // initialize custom keyboard
            let keyboardView = Keyboard(frame: CGRect(x: 0, y: 0, width: 0, height: 300))
            keyboardView.delegate = self // the view controller will be notified by the keyboard whenever a key is tapped
    
            // replace system keyboard with custom keyboard
            textField.inputView = keyboardView
        }
    
        // required method for keyboard delegate protocol
        func keyWasTapped(character: String) {
            textField.insertText(character)
        }
    }
    
  • 请注意,视图控制器采用了我们上面定义的KeyboardDelegate协议。

常见错误

如果您收到EXC_BAD_ACCESS错误,可能是因为您将视图的自定义类设置为Keyboard.swift而不是为nib文件的所有者执行此操作。

选择Keyboard.nib,然后选择File's Owner。

输入图片说明

确保根视图的自定义类为空。

输入图片说明


1
@Piraba,@IBAction func keyTapped 被调用了吗?如果没有,那么您可能没有正确连接 IBActions。如果是,则可能没有正确设置委托。 - Suragch
1
@Suragch,我在let keyboardView = Keyboard(frame: CGRect(x: 0, y: 0, width: 0, height: 300))上遇到了问题。我不知道是Swift的最新更新引起的。由于“width”最初设置为0,所以按钮没有响应touch up inside事件,即使视图自动调整到全宽并且可见。我将调用更改为XIB中框架的最小尺寸以包括按钮,一切都运行得很完美。感谢您的帖子!这正是我要找的。 - McInvale
@Daniel,我有一段时间没有看过这个了。我不太确定你在说什么。调用self.delegate?.keyWasTapped(sender.titleLabel!.text!)会发送“字符”(它是一个String,可能命名不太好),对吧? - Suragch
1
建议使用现有的 UIKeyInput 协议,而不是定义自己的协议。 - Rob
1
@Greg,我已经有一段时间没有开发iOS IMEs了,但你可以研究一下我之前做的这个源代码:https://github.com/suragch/MongolianIPAKeyboardContainer。那是一个系统键盘。而这个则包括一个应用内键盘:https://github.com/suragch/Chimee-iOS。 - Suragch
显示剩余17条评论

33
关键是使用现有的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)

上面的内容比较简单,但它阐明了这个想法:创建自己的输入视图并使用UIKeyInput协议将键盘输入传递给控件。
另外请注意使用accessibilityTraits来获取正确的“口头内容”»“朗读屏幕”行为。如果您在按钮中使用图像,请确保也设置accessibilityLabel。

2
我想在这个键盘上添加一个“完成”按钮,我已经添加了它,但现在在操作时如何取消键盘。 - Protocol
2
@RahulPhate - 将targetUIKeyInput? 更改为 (UIResponder & UIKeyInput)?(也要对 init 方法进行相应的更改)。然后您可以在按钮处理程序中调用 target?.resignFirstResponder() - Rob
2
inputView 的想法是使用自定义的键盘/视图来替代标准键盘。但是,如果您不希望它出现在通常的键盘位置,那么也许您根本不想使用 inputView,而是在某个视图中放置一堆按钮,当您点击它们时更新一些标签。如果您有其他问题,我建议在此答案下的评论可能不是讨论的正确场所。如果您在做一些研究后没有找到任何东西,只需发布您自己的问题即可... - Rob
2
@Elin 如果您不实例化按钮,就没有任何东西可以点击。哈哈。如果您泄漏了这些按钮,很可能是因为您泄漏了设置此“inputView”的UITextFieldUITextView。如果您泄漏了视图控制器,则可能会发生这种情况,该视图控制器用于使用其视图/故事板/ Nib。但问题不在按钮的实例化上,无疑是在视图和/或视图控制器层次结构中更高的位置。但我刚刚测试了一下,按钮的实例化和释放都很好。简而言之,您的问题在其他地方。 - Rob
2
是的,您希望每个控件都有一个不同的键盘实例(以便每个控件可以与其相应的 UITextField 通信)。因此,如果您的每个键盘上有十二个按钮和五个文本字段,那么是的,您需要实例化六十个按钮。 - Rob
显示剩余7条评论

12

在Suragch的答案基础上,我需要一个完成和退格按钮,如果你像我这样是个新手,这里可能会遇到一些错误,以下是我解决这些错误的方法。

出现EXC_BAD_ACCESS错误? 我包含了:

@objc(classname)
class classname: UIView{ 
}

我解决了我的问题,但是Suragch的更新答案似乎以更合适/正确的方式解决了这个问题。

出现SIGABRT错误? 另一个愚蠢的事情是将连接拖错了方向,导致了SIGABRT错误。不要从函数拖到按钮,而应该从按钮拖到函数。

添加"完成"按钮 我在keyboard.swift中的协议中添加了这个:

protocol KeyboardDelegate: class {
    func keyWasTapped(character: String)
    func keyDone()
}

然后将我的完成按钮与keyboard.swift连接一个新的IBAction,就像这样:

@IBAction func Done(sender: UIButton) {
    self.delegate?.keyDone()
}

然后跳回到我使用此键盘的viewController.swift,并在keyWasTapped函数之后添加以下内容:

and then jumped back to my viewController.swift where i am using this keyboard and added this following after the function keyWasTapped:

func keyDone() {
    view.endEditing(true)
}

添加删除键 这个问题曾让我困扰了很久,因为你必须在viewDidLoad()方法中将textField.delegate设置为self(稍后会展示)。

第一步:在keyboard.swift中向协议添加backspace()函数:

protocol KeyboardDelegate: class {
    func keyWasTapped(character: String)
    func keyDone()
    func backspace()
}

第二步:添加一个类似于Done操作的新IBAction:

@IBAction func backspace(sender: UIButton) {
    self.delegate?.backspace()
}

第三步:现在转到viewController.swift文件,在此处将NumberPad键盘显示出来。

重要提示:在viewDidLoad()方法中设置所有将使用此键盘的文本字段。因此,您的viewDidLoad()应该看起来像这样:

 override func viewDidLoad() {
    super.viewDidLoad()

    self.myTextField1.delegate = self
    self.myTextField2.delegate = self

    // initialize custom keyboard
    let keyboardView = keyboard(frame: CGRect(x: 0, y: 0, width: 0, height: 240))
    keyboardView.delegate = self // the view controller will be notified by the keyboard whenever a key is tapped

    // replace system keyboard with custom keyboard
    myTextField1.inputView = keyboardView
    myTextField2.inputView = keyboardView
}

我不确定是否有一种方法可以将此操作应用于视图中的所有textField。这将非常方便...

第四步: 仍在viewController.swift中,我们需要添加一个变量和两个函数。代码如下:

var activeTextField = UITextField()

func textFieldDidBeginEditing(textField: UITextField) {
    print("Setting Active Textfield")
    self.activeTextField = textField
    print("Active textField Set!")
}

func backspace() {
    print("backspaced!")
    activeTextField.deleteBackward()
}

这里正在解释发生了什么:

  1. 您创建一个变量来存储文本字段。
  2. 当"textFieldDidBeginEditing"被调用时,它设置变量以便知道我们正在处理哪个文本字段。我添加了大量的打印()以便知道所有内容都已被执行。
  3. 我们的退格功能然后检查我们正在处理的文本字段并使用.deleteBackward()。这会删除光标前的字符。

现在您应该可以开始处理业务了。非常感谢Suragchs帮助我实现这一点。


1
使用 textField.deleteBackward() 进行退格操作。如果您感兴趣,这里有一个演示视图控制器,以及这个是我在其中使用的键盘之一。它展示了我自从最初开始使用此示例以来所取得的进展。最终,我放弃了 nibs(出于无关原因),并仅通过编程方式布局,尽管我不一定建议您这样做。 - Suragch
如果有两个文本框,第一个文本框需要自定义键盘,而第二个文本框可以使用iPhone的默认键盘。我想隐藏默认键盘,然后显示默认键盘。 - marlonpya
我不确定我理解你的问题,marlonpya。如果你想要一个textfield的默认键盘,那么你不需要在viewDidLoad()中进行设置。然后,如果你确实想要另一个textfield的自定义键盘,在viewDidLoad()中你必须将textfield的代理设置为self,并按照上面第三步所述,用自定义键盘替换键盘。 - Steve

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接