Edited at

iOSカスタムキーボードの作り方


導入

iOS8からカスタムキーボードが解禁となったが、その開発方法を網羅的にまとめてある場所がないと思われる。iOSカスタムキーボード開発に関するあらゆる知見をここに集約したい。

本記事では以下の項目について取り上げる


  • iOSカスタムキーボードの基本

  • カスタムキーボードのプロジェクト立ち上げ

  • キーのレイアウト方法

  • キーを押して文字を挿入する方法

  • キーを押して文字を削除する方法(連続消し含む)

  • キーを押した時に短い音を鳴らす方法

  • キーを押した時に長い音を鳴らす方法

  • キーボードの高さを変更する方法

  • 画面の回転を検知する方法

  • 設定アプリを作る方法(App GroupをONにしてUserDefaultsを使う方法)

  • iPhone Xに対応させる方法

  • スペルミスチェックや補完をする方法

  • 日本語入力(かな漢字変換)をする方法

  • フルアクセスがONかOFFかチェックする方法

  • キーボードの左端のキーの反応速度を速くする方法(2018/1/9 追記)

  • キーボードの下端をスワイプしてもコントロールセンターを出現させない方法(2018/1/19 追記)

  • 実装不可能だと思われること

本記事はカスタムキーボード開発における全てのハマりどころを網羅できているわけではない。コメント等にてさらなる情報の集約を期待したい。

※本記事はXcodeをIDEとして話を進めていきます。Xamarinでやりたいという人はこちらの記事を参考にしてください。また、Androidのカスタムキーボードを作りたいよという方はこちらの記事をどうぞ。


iOSカスタムキーボードの基本

iOSのカスタムキーボードはKeyboard ExtensionというApp Extensionの一つとその収容アプリによって構成されます。収容アプリをiOSデバイスにダウンロードし、環境設定から当該キーボードを追加することで有効になります。つまり、キーボードだけをパッケージングしてAppStoreで配布するということはできません。収容アプリとKeyboard Extensionは別々のtargetとなるのでイメージとしては独立した2つのアプリケーションを用意するといった感じになります。iOSカスタムキーボードのリリース要件としては、キーボードとして0~9の数字を入力することができること、次のキーボードに切り替える用のGlobeボタンが搭載されていること、収容アプリによって設定を切り替えることができること、ゲームなど余計な機能は搭載されていないこと、サポートサイトおよびプライバシーポリシーサイトが用意されていることなどがあります。ちなみにKeybaord ExtensionはMaxで30MB以内の処理負荷に収まる必要があるらしいです(ソース不確か)とにかく挙動が軽くないとダメらしいです。


カスタムキーボードのプロジェクト立ち上げ


  1. まず、通常通りにSingle Viewアプリケーションを立ち上げます。こちらが収容アプリケーションになります。


  2. 続いてFile -> New -> Target -> iOS -> Custom Keybaord Extension からKeyboard Extensionのターゲットを追加します。


  3. この際、スキームのアクティベートをしろと言ってくるのでActivateを押します。

    project6.png


  4. 構成が以下のようになればOKです。

    構成.png


ViewControoler.swiftが収容アプリケーションのメイン、KeyboardViewController.swiftがカスタムキーボードのメインです。

KeyboardViewController.swiftには次のキーボードに切り替えるためのGlobeボタンの例が記述してあります。

この中で重要なのは、handleInputModeList(from:with:)self.textDocumentProxyの二つです。

とりあえずiOSシミュレーターでも実機でも良いので実行してみて、キーボードを環境設定から有効にして出現させることができればひと段落です。


キーの追加とレイアウト方法


追加

デフォルトで記述されているnextKeyboardButtonを見ればわかりますが、UIButtonのインスタンスを生成して、タイトルを設定して、ボタンを押した時のアクションをaddTargetで指定してあげ、self.viewaddSubviewで追加してあげれば追加はできます。ここで、自分でUIButtonを継承したカスタムボタンクラスを用意していた場合は、そのインスタンスを生成してaddSubviewすればOKです。追加先がself.inputViewではなく、self.viewで良いのか?という疑問を抱く人がいるかもしれませんが、このUIInputViewControllerのViewは、inputViewとして読み込まれるのでこれで良いのです。


レイアウト

まずは、ボタンのtranslatesAutoresizingMaskIntoConstraintsfalseにします。そうしないと、レイアウトが自由に組めません。コンパイルは通りますが、実行時に大量の警告を表示してうざかったり、表示が変になったりします。(layoutのconstraintsの競合が大量に発生)

そして、もう一つ重要なこととして、レイアウトのルールを決めるコードは、レイアウトにまつわる全てのUIパーツがself.viewaddSubviewされてからしてください。そうでないときちんと発動されません。

デフォルトの例では、

self.nextKeyboardButton.leftAnchor.constraint(equalTo: self.view.leftAnchor).isActive = true

のようにレイアウトのルール決めを行なっていますが、これ以外にも方法はいくつかあります。

self.view.addConstraint(NSLayoutConstraint(item: key1, attribute: .leading, relatedBy: .equal, 

toItem: self.view, attribute: .leading,
multiplier: 1.0, constant: 0.0))

self.view.addConstraints([NSLayoutConstraint(item: key1, attribute: .top, relatedBy: .equal,
toItem: self.view, attribute: .top,
multiplier: 1.0, constant: 0.0),
NSLayoutConstraint(item: key1, attribute: .width, relatedBy: .equal,
toItem: self.view, attribute: .width,
multiplier: 1 / 3, constant: 0.0),
NSLayoutConstraint(item: key1, attribute: .height, relatedBy: .equal,
toItem: nil, attribute: .notAnAttribute,
multiplier: 1.0, constant: 40.0)])

親となるself.viewに対してconstraintを追加する方法です。引数の意味は大体は予想つくと思いますが、補足をします。幅や高さなどの場合にはmultiplierを指定して相対的な大きさにすることができます。また、constantに値を入れることで大きさやUIパーツどうしの間隔を値で指定することができます。ここは左や上からの距離を指定する場合は正の数、右や下からの距離を指定する場合は負の数を入れなければならないので要注意です。


キーを押して文字を挿入する方法

キーを押した時の処理はaddTargetにてメソッドを指定します。

@objc func pushA() {

self.textDocumentProxy.insertText("A")
}

文字の挿入にはtextDocumentProxyの持つinsertTextメソッドを使います。例えば、改行やタブ入力などを行いたいときはエスケープシーケンスを用いればOKです。(例:"\n", "\t")注意することとしては、Swift4から#selectorを用いるときは@objcが必要となりましたのでつけ忘れずに。


キーを押して文字を削除する方法(連続消し含む)


単純に一文字消す場合

@objc func delete() {

self.textDocumentProxy.deleteBackward()
}


ボタンを押している間連続で消す方法

var deleteBtn = UIButton(type: .system)

//タイトルとか設定...
//ボタンを押した瞬間のイベントと離した瞬間のイベントの登録
deleteBtn.addTarget(self, action: #selector(self.deleteDown), for: .touchDown)
deleteBtn.addTarget(self, action: #selector(self.deleteUp), for: [.touchUpInside, .touchUpOutside])

@objc func deleteDown() {
proxy.deleteBackward()
deleteStartTime = Date()
deleteTimer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true, block: { (timer) in
let span: TimeInterval = timer.fireDate.timeIntervalSince(self.deleteStartTime!)
if span > 0.4 {
self.proxy.deleteBackward()
}
})
}

@objc func deleteUp() {
deleteTimer?.invalidate()
}

spanはボタンを押してから連続削除を開始するまでの時間です。


キーを押した時に短い音を鳴らす方法

AudioToolboxAudioServicesPlaySystemSound()を使いましょう。



AudioServicesPlaySystemSound(1155) //削除音

AudioServicesPlaySystemSound(1156) //普通のクリック音

鳴らせる音に関してはここを参照してください.

ここにない番号の音声もあるので、頑張って調べてみてください。

※上記のコード、ただ書くだけでは発動しないことがあります。

以下のようにUIInputViewで音を鳴らす許可を与えてください。

extension UIInputView: UIInputViewAudioFeedback {

open var enableInputClicksWhenVisible: Bool {
return true
}
}


キーを押した時に長い音を鳴らす方法

OpenALを使う方法でもAVAudioEngineをつかう方法でもなんでも良いですが、カスタムキーボード上で音(曲?)を鳴らす場合は、「フルアクセスの許可」を行わない限りキーボードがクラッシュしてしまいます。音を鳴らす場合、環境設定 -> キーボード -> 該当キーボード名 -> フルアクセスの許可 をオンにしてください。

音を鳴らす関連の記事を投稿してあります。

Swift4 OpenALでcaf音源を鳴らす方法

Swift4 綺麗なサイン波音をその場で生成して鳴らす方法

動画の再生中、Musicアプリでの音楽再生中に音を止めずにキーのクリック音などを再生したい場合は以下のようにします。

override func viewWillAppear(_ animated: Bool) {

super.viewWillAppear(animated)
if self.hasFullAccess {
let audioSession = AVAudioSession.sharedInstance()
do {
try audioSession.setCategory(AVAudioSessionCategoryAmbient)
} catch {

}
}
}


キーボードの高さを変更する方法

override func viewDidLoad() {

super.viewDidLoad()

let height: CGFloat = 150.0
let constraintH = NSLayoutConstraint(item: self.view, attribute: .height,
relatedBy: .equal, toItem: nil, attribute: .notAnAttribute,
multiplier: 1.0, constant: height)
constraintH.priority = UILayoutPriority(rawValue: 990)
self.view.addConstraints([constraintH])
}

高さを変更する際の要は.priorityです。これを大きな値にすることでconstraintの優先順位を上げ、警告やエラーなく高さを変更することができます。このpriorityの値についてはStack Overflowなどで様々に書いてありますが、999にすればいいというのは罠です。クラッシュします。990程度が良いと実験してわかりました。


画面の回転を検知する方法

viewWillTransitionを使います.

override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {

Swift.print("transition \(size)")
}

sizeで回転後のキーボードの大きさがわかります.(高さを指定している時にきちんと動くかは未確認)


設定アプリを作る方法(App GroupをONにしてUserDefaultsを使う方法)

収容アプリとKeyboard ExtensionのCapabilitiesの中のApp GroupsをOnにして設定してください。詳しいことはここを参照してください。App Groups Identifierを手に入れたらほとんど普段のUserDefaultsと同様の使い方ができます。(Identifierの接頭辞にはgroup.をつける事が推奨されています)

var defaults: UserDefaults = UserDefaults(suiteName: "group.com.sample.customkeyboard")

//set default value
defaults.register(defaults: ["settingBool" : true, "settingInt" : 5, "settingDouble" : 3.14])

//getValue
let value1: Bool = defaults.bool(forKey: "settingBool")
let value2: Int = defaults.integer(forKey: "settingInt")
let value3: Double = defaults.double(forKey: "settingDouble")

//setValue
defaults.set(false, forKey: "settingBool")
defaults.set(3, forKey: "settingInt")
defaults.set(8.45, forKey: "settingDouble")


iPhone X/XSに対応させる方法

iPhone X/XSのキーボードは画面サイズが広くなったせいで、Globeボタンや絵文字ボタンが下の空いているスペースに勝手に表示されるようになりました。しかし、Appleさんによると、Globeボタンが二つ存在していると誤解を生んでよくないのでiPhone X/XSのときはGlobeボタンを表示させないようにしろとのことです。Globeボタンを表示する必要があるかないかは、self.needsInputModeSwitchKeyで判断可能です。これのフラグがfalseの時はGlobeボタンをaddSubviewしないようにしてください。

※追記:このフラグはviewDidAppear以降で確認しないと値が確定していないようです.viewDidAppear内で確認しましょう.


スペルミスチェックや補完をする方法

UITextCheckerとかUILexiconを使ってください。ただし、日本語には対応していませんので日本語に対応させる場合は、GoogleなどのAPIを使ってください。

参考

UITextCheckerでスペルチェック

UILexicon を使ってカスタムキーボードに用語集を表示する


日本語入力(かな漢字変換)をする方法

ライブラリを作りました(発展途上中)ので使ってやってください.

iOS向けかな漢字変換ライブラリ:Kaede Project

Googleのmozcを使うという方法もありますが,赤ちゃんにコーヒーを飲ませるぐらい苦行なので覚悟してとりかかってください.この案件でKishikawa先生を知りました.

参考:SwiftでMozc for iOSを実装したいのですが、C++(ヘッダーファイル)の読み込みで躓いています。


フルアクセスがONかOFFかチェックする方法

func checkFullAccess() -> Bool {

let pb = UIPasteboard.general
if pb.hasStrings || pb.hasURLs || pb.hasColors || pb.hasImages {
return true
} else {
UIPasteboard.general.string = ""
return UIPasteboard.general.hasStrings
}
}

上記の方法を用いなくてもフルアクセスかどうか判断できるメンバが存在していました...。

self.hasFullAccess


キーボードの左端のキーの反応速度を速くする方法

原理はよくわかりませんが、これで速くなります。

override func viewDidAppear(_ animated: Bool) {

super.viewDidAppear(animated)
let window = self.view.window!
let gr0 = window.gestureRecognizers![0] as UIGestureRecognizer
let gr1 = window.gestureRecognizers![1] as UIGestureRecognizer
gr0.delaysTouchesBegan = false
gr1.delaysTouchesBegan = false
}


キーボードの下端をスワイプしてもコントロールセンターを出現させない方法

override func preferredScreenEdgesDeferringSystemGestures() -> UIRectEdge {

return .bottom
}


実装不可能だと思われること


キーボードの左端のキーの反応速度を速くすること

ForceTouch対応機種のiOS機器の場合、画面左端からのスワイプでマルチタスクの切り替えというジェスチャが優先されてしまう。そのためキーボード左端のキーの反応速度が鈍くなる。現在環境設定で3DタッチをOFFにする以外の方法で左端のキーの反応速度をあげることができない。キーボード側からForceTouch可能領域の指定ができれば良いのだが。

↑方法を見つけました。


・キーボードの背景を完全に透明にすること

透明なキーボードを画面全体に表示させようと試みたが、inputViewの背景を透明にしても、Layerを透明にしても、SuperViewやSuperSuperViewの背景を透明にしようとしてもUIWindowをとうめいにしようとしても実現できなかった。キーボード背景色のあのグレー色を取り除くことはできないのかもしれない。


・挿入中の未確定文字列の背景を変化させること

デフォルトのキーボードでは当たり前のようにできている機能だが、実装方法が全くわからない。

デフォルトのやつは文字列が選択状態になっているようにも見えるが、挿入中のテキストボックスのインスタンスをいじることができないためおそらく不可能。


参考

【Swift】自作カスタムキーボードで醜態を晒さないためにチェックしたい6つの起動パターン