webdevqa.jp.net

A Swiftデータ入力のカスタムビューの例(カスタムアプリ内キーボード)

ゴール

インストールする必要があるシステムキーボードではなく、アプリ内でのみ使用されるカスタムキーボードを作成したい。

私が読んで試したこと

ドキュメント

上記の最初の記事には次のように記載されています。

カスタムのシステム全体のキーボードが実際に開発したいものであることを確認してください。アプリ専用の完全カスタムキーボードを提供したり、アプリ専用のカスタムキーでシステムキーボードを補完したりするために、iOS SDKには他の優れたオプションが用意されています。 iOS用テキストプログラミングガイドのデータ入力のカスタムビューで、カスタム入力ビューと入力アクセサリビューについてお読みください。

それが私を上記の2番目の記事に導いたものです。しかし、その記事には、始めるのに十分な詳細がありませんでした。

チュートリアル

上記のリストの2番目のチュートリアルから、機能するキーボードを取得できました。ただし、 データ入力のカスタムビュー のドキュメントで説明されているように、アプリ内のみのキーボードを作成する方法を示すチュートリアルは見つかりませんでした。

スタックオーバーフロー

また、現在の質問に答える途中でこれらの質問をしました(そして答えました)。

質問

アプリ内のカスタムキーボードの最小限の例(ボタンが1つでもある)はありますか?チュートリアル全体を探しているのではなく、自分自身で拡張できる概念の証明を探しているだけです。

35
Suragch

重要なのは、UIKeyInputが既に準拠している既存の UITextField プロトコルを使用することです。次に、キーボードビューは insertText() および deleteBackward() をコントロールに送信するだけです。

次の例は、カスタム数値キーボードを作成します。

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

class NumericKeyboard: UIView {
    weak var target: UIKeyInput?

    var numericButtons: [DigitButton] = (0...9).map {
        let button = DigitButton(type: .system)
        button.digit = $0
        button.setTitle("\($0)", for: .normal)
        button.titleLabel?.font = UIFont.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 = UIFont.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
    }()

    init(target: UIKeyInput) {
        self.target = target
        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 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)

        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)

その結果:

enter image description here

上記はかなり原始的ですが、アイデアを示しています:入力ビューを所有し、UIKeyInputプロトコルを使用してキーボード入力をコントロールに伝えます。

また、accessibilityTraitsを使用して正しい「音声コンテンツ」"「画面を話す」動作を取得することにも注意してください。また、ボタンに画像を使用する場合は、accessibilityLabelも必ず設定してください。

4
Rob

enter image description here

これは基本的なアプリ内キーボードです。同じ方法を使用して、ほぼすべてのキーボードレイアウトを作成できます。実行する必要がある主なものは次のとおりです。

  • 所有者がUIViewサブクラスを含む.Swiftファイルである.xibファイルにキーボードレイアウトを作成します。
  • UITextFieldにカスタムキーボードを使用するように伝えます。
  • デリゲートを使用して、キーボードとメインView Controllerの間で通信します。

.xibキーボードレイアウトファイルを作成する

  • XcodeでFile> New> File ...> iOS> User Interface> Viewに移動して、.xibファイルを作成します。
  • 私は私のKeyboard.xibを呼び出しました
  • 必要なボタンを追加します。
  • 自動レイアウト制約を使用して、キーボードのサイズに関係なく、ボタンのサイズがそれに応じて変更されるようにします。
  • (ルートビューではなく)ファイルの所有者をKeyboard.Swiftファイルに設定します。これはエラーの一般的な原因です。最後の注を参照してください。

.Swift UIViewサブクラスキーボードファイルを作成する

  • XcodeでFile> New> File ...> iOS> Source> Cocoa 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 = 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メソッドへのドラッグを制御して、それらをすべてフックします。

  • プロトコルと委任コードに注意してください。デリゲートの仕組みに関する簡単な説明については、 この回答 を参照してください。

View Controllerをセットアップする

  • メインストーリーボードにUITextFieldを追加し、IBOutletを使用してView Controllerに接続します。 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)
        }
    }
    
  • View Controllerは、上記で定義したKeyboardDelegateプロトコルを採用していることに注意してください。

一般的なエラー

EXC_BAD_ACCESSエラーが発生している場合、おそらく、nibファイルの所有者に対してこれを行うのではなく、ビューのカスタムクラスをKeyboard.Swiftとして設定しているためです。

Keyboard.nibを選択し、ファイルの所有者を選択します。

enter image description here

ルートビューのカスタムクラスが空白であることを確認してください。

enter image description here

68
Suragch

Suragchの答えに基づいて、完了ボタンとバックスペースボタンが必要でした。あなたが私のような初心者であれば、遭遇する可能性のあるエラーとその解決方法がここにあります。

EXC_BAD_ACCESSエラーを取得しますか?含まれています:

@objc(classname)
class classname: UIView{ 
}

私の問題を修正しましたが、Suragchの更新された答えはこれをより適切/正しい方法で解決しているようです。

Getting SIGABRT Error?もう1つの愚かなことは、接続を間違った方向にドラッグして、SIGABRTエラーを引き起こしていました。関数からボタンにドラッグするのではなく、ボタンを関数にドラッグします。

完了ボタンの追加これをkeyboard.Swiftのプロトコルに追加しました:

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

次に、新しいIBActionをdoneボタンからkeyboard.Swiftに接続しました。

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

そして、このキーボードを使用しているviewController.Swiftに戻ってジャンプし、関数keyWasTappedの後にこれを追加しました:

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

バックスペースの追加 viewDidLoad()メソッド(後述)でtextField.delegateをselfに設定する必要があるため、これは非常につまずきました。

最初:keyboard.Swiftでプロトコルfunc backspace()に追加します:

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

Second:Doneアクションに似た新しいIBActionを接続します:

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

3番目:NumberPadが表示されるviewController.Swiftへ。

重要: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
}

ビュー内にあるすべてのtextFieldに対してこれを行う方法がある場合、どのようにすればよいのかわかりません。これは便利でしょう...

Forth:まだviewController.Swiftに変数と2つの関数を追加する必要があります。次のようになります。

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を扱っているかを認識します。多くのprints()を追加したので、すべてが実行されていることがわかります。
  3. バックスペース関数は、処理しているtextFieldをチェックし、.deleteBackward()を使用します。これにより、カーソルの直前の文字が削除されます。

そして、あなたはビジネスをするべきです。これを実現させてくれたSuragchsに感謝します。

9
Steve