Пример Swift пользовательских представлений для ввода данных (настраиваемая клавиатура в приложении)

Цель

Я хочу создать пользовательскую клавиатуру, которая используется только в моем приложении, а не системную клавиатуру, которую необходимо установить.

Что я прочитал и попробовал

Документация

Первая статья выше гласит:

Убедитесь, что пользовательская общесистемная клавиатура действительно является тем, что вы хотите разработать. Чтобы предоставить полностью настраиваемую клавиатуру только для вашего приложения или дополнить системную клавиатуру настраиваемыми клавишами только в вашем приложении, iOS SDK предоставляет другие, более лучшие варианты. Прочитайте о пользовательских представлениях ввода и вспомогательных видах ввода в Пользовательских представлениях для ввода данных в Руководстве по программированию текста для iOS.

Вот что привело меня ко второй статье выше. Однако в этой статье не было достаточно деталей, чтобы начать меня.

Учебники

Я смог получить рабочую клавиатуру из второго урока в списке выше. Однако я не смог найти никаких учебных пособий, в которых показано, как создать встроенную клавиатуру только в приложении, как описано в документации по пользовательским представлениям для ввода данных.

Переполнение стека

Я также задавал (и отвечал) эти вопросы на пути к ответу на текущий вопрос.

Вопрос

У кого-нибудь есть минимальный пример (даже с одной кнопкой) пользовательской клавиатуры в приложении? Я не ищу целый учебник, просто доказательство концепции, которую я могу расширить на себя.

2 ответа

Решение

Это базовая клавиатура в приложении. Тот же метод может быть использован для создания практически любой раскладки клавиатуры. Вот основные вещи, которые необходимо сделать:

  • Создайте раскладку клавиатуры в файле.xib, владельцем которого является файл.swift, содержащий UIView подкласс.
  • Скажи UITextField использовать пользовательскую клавиатуру.
  • Используйте делегата для связи между клавиатурой и контроллером основного вида.

Создайте файл раскладки клавиатуры.xib

  • В Xcode перейдите в Файл> Создать> Файл... > iOS > Интерфейс пользователя> Вид, чтобы создать файл.xib.
  • Я назвал мой Keyboard.xib
  • Добавьте кнопки, которые вам нужны.
  • Используйте автоматические ограничения макета, чтобы независимо от того, какого размера клавиатура, размер кнопок будет соответственно изменяться.
  • Установите в качестве владельца файла (не корневого представления) файл Keyboard.swift. Это распространенный источник ошибок. Смотрите примечание в конце.

Создайте файл клавиатуры подкласса.swift UIView

  • В Xcode зайдите в Файл> Создать> Файл... > iOS > Источник> Какао Touch Class, чтобы создать файл.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 = NSBundle.mainBundle().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 на @IBAction метод в файле.swift, чтобы соединить их всех.

  • Обратите внимание, что протокол и код делегата. Посмотрите этот ответ для простого объяснения того, как работают делегаты.

Настройте View Controller

  • Добавить UITextField к основной раскадровке и подключите его к вашему контроллеру представления с IBOutlet, Назови это textField,
  • Используйте следующий код для View Controller:

    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, а не сделали это для владельца файла пера.

Выберите Keyboard.nib, а затем выберите Владелец файла.

Убедитесь, что пользовательский класс для корневого представления пуст.

Ключ в том, чтобы использовать существующие UIKeyInput протокол, к которому UITextFieldуже соответствует. Тогда ваше представление клавиатуры нужно только отправить insertText() а также deleteBackward() к контролю.

В следующем примере создается настраиваемая цифровая клавиатура:

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

class NumericKeyboard: UIView {
    weak var target: UIKeyInput?
    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, 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) {
        target?.insertText("\(sender.digit)")
    }

    @objc func didTapDecimalButton(_ sender: DigitButton) {
        target?.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
    }
}

Тогда ты можешь:

textField.inputView = NumericKeyboard(target: textField)

Это дает:

Или, если вам нужен десятичный разделитель, вы можете:

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

Вышеупомянутое довольно примитивно, но оно иллюстрирует идею: создайте собственное представление ввода и используйте UIKeyInput протокол для передачи данных с клавиатуры на элемент управления.

Также обратите внимание на использование accessibilityTraitsдля получения правильного поведения "Разговорный контент" "" Говорить на экране ". И если вы используете изображения для своих кнопок, не забудьте установитьaccessibilityLabel, тоже.

Основываясь на ответе Сурагча, мне понадобилась кнопка "Готово" и "Backspace", и если вы такой нуб, как я, то здесь есть некоторые ошибки, с которыми вы можете столкнуться, и способ, которым я их решил.

Получаете ошибки EXC_BAD_ACCESS? Я включен:

@objc(classname)
class classname: UIView{ 
}

исправил мою проблему, однако обновленный ответ Сурагча, похоже, решил эту проблему более подходящим / правильным способом.

Получение SIGABRT Ошибка? Еще одна глупая вещь - перетаскивание соединений неправильным способом, вызывающее ошибку SIGABRT. Не перетаскивайте из функции на кнопку, а вместо кнопки на функцию.

Добавление кнопки "Готово" Я добавил это в протокол в keyboard.swift:

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

Затем подключил новый IBAction от моей кнопки "Готово" к клавиатуре: вот так:

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

а затем отскочил обратно в мой viewController.swift, где я использую эту клавиатуру, и добавил следующее после функции keyWasTapped:

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

Добавление Backspace Это меня сильно смутило, потому что вы должны установить для textField.delegate значение self в методе viewDidLoad() (см. Ниже).

Во-первых: в файле keyboard.swift добавьте в протокол func backspace():

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

Второе: подключите новый IBAction, похожий на действие Done:

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

Третье: к viewController.swift, где появляется NumberPad.

Важный: в viewDidLoad() установите все textFields, которые будут использовать эту клавиатуру. Так что ваш 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
}

Я не уверен, как, если есть способ просто сделать это для всех textFields, которые находятся в представлении. Это было бы удобно...

Далее: все еще в 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. Вы делаете переменную, которая будет содержать textField.
  2. Когда вызывается textFieldDidBeginEditing, он устанавливает переменную, чтобы он знал, с каким textField мы имеем дело. Я добавил много print (), поэтому мы знаем, что все выполняется.
  3. Наша функция возврата затем проверяет textField, с которым мы имеем дело, и использует.deleteBackward(). Это удаляет непосредственный символ перед курсором.

И ты должен быть в бизнесе. Большое спасибо Suragchs за помощь в этом.

Другие вопросы по тегам