導入
iOS8 からカスタムキーボードが解禁となりましたが、その開発方法を網羅的にまとめてある場所がありません。本記事では iOS カスタムキーボード開発に関するあらゆる知見をここに集約していきたいと思っています。
- iOS カスタムキーボードの基本
- カスタムキーボードのプロジェクト立ち上げ
- キーのレイアウト方法
- キーを押して文字を挿入する方法
- キーを押して文字を削除する方法
- カーソルの位置を動かす方法
- キーを押した時に音を鳴らす方法
- 画面の向きに応じてレイアウトを変更する方法
- フルアクセスのオンオフをチェックする方法
- 設定アプリを作る方法
- 収容アプリ名とキーボード名の付け方
- ノッチあり iPhone に対応させる方法
- スペルミスチェックや補完をする方法
- 日本語入力をする方法
- 左右端のキーの反応速度を正常にする方法
- 下端をスワイプしてもコントロールセンターを出現させない方法
- 実装不可能だと思われること
※ 本記事は Xcode を IDE として話を進めていきます。Xamarin でやりたいという方はこちらの記事を参考にしてください。また、Android のカスタムキーボードを作りたいよという方はこちらの記事をどうぞ。
iOS カスタムキーボードの基本
iOS のカスタムキーボードは Keyboard Extension という App Extension の1つとその収容アプリによって構成されます。収容アプリをデバイスにダウンロードし、環境設定から当該キーボードを追加することで有効になります。つまり、キーボードだけをパッケージングして App Store で配布するということはできません。収容アプリと Keyboard Extension は別々の target となるのでイメージとしては独立した2つのアプリケーションを用意するといった感じになります。
リリース要件
-
キーボードとして0〜9の数字を入力できること(ソース) - 次のキーボードに切り替えるためのボタンが搭載されていること
- 収容アプリによって設定を切り替えることができること
- ゲームなど余計な機能は搭載されていないこと
- サポートサイトおよびプライバシーポリシーサイトが用意されていること
などがあります(Review Guilelinesも参考に)。ちなみに Keybaord Extension は最大で 30MB 以内の処理負荷に収まる必要があるらしいです(ソース)。
カスタムキーボードのプロジェクト立ち上げ
-
まず、通常通りに Single View アプリケーションを立ち上げます。こちらが収容アプリケーションになります。
-
続いて File -> New -> Target -> iOS -> Custom Keybaord Extension から Keyboard Extension のターゲットを追加します。
ViewControoler.swift が収容アプリケーションのメイン、KeyboardViewController.swift がカスタムキーボードのメインです。KeyboardViewController.swift には次のキーボードに切り替えるためのボタンの例が記述してあります。
この中で重要なのは、handleInputModeList(from:with:)
とself.textDocumentProxy
の2つです。とりあえず iOS シミュレーターでも実機でも良いので実行してみて、キーボードを環境設定から有効にして出現させることができればひと段落です。
キーのレイアウト方法
キーの追加
デフォルトで記述されているnextKeyboardButton
を見れば基本的なキーの追加方法がわかります。UIButton のインスタンスを生成して、タイトルを設定して、ボタンを押した時のアクションをaddTarget
で指定してあげ、self.view
にaddSubview
で追加してあげれば追加はできます。ここで、自分で UIButton を継承したカスタムボタンクラスを用意していた場合は、そのインスタンスを生成してaddSubview
すれば OK です。追加先がself.inputView
ではなく、self.view
で良いのか?という疑問を抱く人がいるかもしれませんが、この UIInputViewController の View は、inputView として読み込まれるのでこれで良いのです。
レイアウトの仕方
まずは、レイアウトを自由に組むためにボタンの設定を変更します。
button.translatesAutoresizingMaskIntoConstraints = false
これをしないとコンパイルは通りますが、実行時に大量の警告を表示してうざかったり、表示が変になったりします(layout の constraints の競合が大量に発生)。
そして、もう一つ重要なこととして、レイアウトのルールを決めるコードは、レイアウトにまつわる全ての UI パーツをaddSubview
してから記述するようにしてください。そうでないとレイアウトがきちんと発揮されません。
デフォルトの例では、
self.nextKeyboardButton.leftAnchor.constraint(equalTo: self.view.leftAnchor).isActive = true
self.nextKeyboardButton.bottomAnchor.constraint(equalTo: self.view.bottomAnchor).isActive = true
のようにレイアウトのルール決めを行なっていますが、これ以外にも方法はいくつかあります。
// 1つだけConstraintを追加する場合
self.view.addConstraint(NSLayoutConstraint(item: key1, attribute: .leading, relatedBy: .equal,
toItem: self.view, attribute: .leading,
multiplier: 1.0, constant: 0.0))
// 複数のConstraintを追加する場合
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 パーツどうしの間隔を値で指定することができます。注意点として、左や上からの距離を指定する場合は正の数、右や下からの距離を指定する場合は負の数を入れなければなりません。
Constraint
を定義するのは、viewWillLayoutSubviews()
の中で行うことをお勧めします。
キーを押して文字を挿入する方法
キーを押した時の処理はaddTarget
にてメソッドを指定します。
button.addTarget(self, action: #selector(pushA), for: .touchUpInside)
@objc func pushA() {
self.textDocumentProxy.insertText("A")
}
文字の挿入にはtextDocumentProxy
の持つinsertText
メソッドを使います。例えば、改行やタブ入力などを行いたいときはエスケープシーケンスを用いれば OK です(例:"\n", "\t"
)。
キーを押して文字を削除する方法
単純に1文字消す場合
@objc func delete() {
self.textDocumentProxy.deleteBackward()
}
ボタンを押している間連続で消す方法
var deleteStartTime: Date?
var deleteTimer: Timer?
let 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() {
self.textDocumentProxy.deleteBackward()
deleteStartTime = Date()
deleteTimer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true, block: { (timer) in
let span: TimeInterval = timer.fireDate.timeIntervalSince(self.deleteStartTime!)
if 0.4 < span {
self.textDocumentProxy.deleteBackward()
}
})
}
@objc func deleteUp() {
deleteTimer?.invalidate()
}
※ span
はボタンを押してから連続削除を開始するまでの時間です。
カーソルの位置を動かす方法
// 1文字分左へ
self.textDocumentProxy.adjustTextPosition(byCharacterOffset: -1)
// 1文字分右へ
self.textDocumentProxy.adjustTextPosition(byCharacterOffset: 1)
キーを押した時に音を鳴らす方法
キーを押した時に短い音を鳴らす方法
AudioToolbox
のAudioServicesPlaySystemSound()
を使いましょう。
AudioServicesPlaySystemSound(1155) //削除音
AudioServicesPlaySystemSound(1156) //普通のクリック音
鳴らせる音に関してはここを参照してください。ここにない番号の音声もあるので、頑張って調べてみてください。
※ 上記のコード、ただ書くだけでは発動しないことがあります。以下のように UIInputView で音を鳴らす許可を与えてください。
extension UIInputView: UIInputViewAudioFeedback {
open var enableInputClicksWhenVisible: Bool {
return true
}
}
キーを押した時に長い音を鳴らす方法
OpenAL
を使う方法でもAVAudioEngine
をつかう方法でもなんでも良いですが、カスタムキーボード上で音(曲?)を鳴らす場合は、「フルアクセスの許可」を行わない限りキーボードがクラッシュしてしまいます。音を鳴らす場合、ユーザにフルアクセスの許可をしてもらう必要があります。フルアクセスの許可をする場所は、環境設定 -> キーボード -> 該当キーボード名 -> フルアクセスの許可
です。
音を鳴らすことに関連する記事を投稿しています。
動画の再生中、Music アプリでの音楽再生中に音を止めずにキーのクリック音などを再生したい場合は以下のようにします。
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
if self.hasFullAccess {
try? AVAudioSession.sharedInstance().setCategory(.ambient, mode: .default)
}
}
※ この処理はviewWillAppear()
より後で行わないと成功しないようです。
画面の向きに応じてレイアウトを変更する方法
viewWillTransition
を用いて回転を検出しUIScreen.main.bounds.size
で縦横を判定します。
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
Swift.print("transition \(size)")
}
func judgeVH() {
let size: CGSize = UIScreen.main.bounds.size
if size.width < size.height {
Swift.print("縦")
} else {
Swift.print("横")
}
}
※ デバイスの種類によってもレイアウトを変えたい時は、UIDevice.current.userInterfaceIdiom
で判別できます。
縦か横かを判定できたら、実際にレイアウトを切り替えられるようにします。予め縦向きと横向き用のレイアウト(制約)を作っておき、そのオンオフを切り替えるという方法が賢いと思います。
// 縦向きのときの制約
let verticalConst: NSLayoutConstraint = button.widthAnchor.constraint(equalToConstant: 100)
// 横向きのときの制約
let horizontalConst: NSLayoutConstraint = buttonwidthAnchor.constraint(equalToConstant: 200)
let size: CGSize = UIScreen.main.bounds.size
if size.width < size.height {
verticalConst.isActive = true
horizontalConst.isActive = false
} else {
verticalConst.isActive = false
horizontalConst.isActive = true
}
フルアクセスのオンオフをチェックする方法
self.hasFullAccess
を用いて判別できます。判別できるのはviewWillAppear()
より後です。
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
if self.hasFullAccess {
Swift.print("フルアクセス許可中")
}
}
設定アプリを作る方法
収容アプリと Keyboard Extension の Capabilities の中の App Groups を ON にしてください。詳しいことはここを参照してください。App Groups Identifier を手に入れたらほとんど普段の UserDefaults と同様の使い方ができます(Identifier の接頭辞には group.をつける事が推奨されています)。
let defaults = 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")
収容アプリ名とキーボード名の付け方
収容アプリと Keyboard Extension は別 target となるので、それぞれに Bundle identifier が割り振られます。
デフォルトだとそれぞれの Bundle name (PRODUCT_NAME)が収容アプリとキーボードの名前となるのですが、それだと、環境設定で表示されるキーボード名が思い通りにいかないことがあります。
例えば、収容アプリのBundle nameがCustomKeyboard
でキーボードのBundle nameがCustomKeyboardExtension
だと、
CustomKeyboardExtension - CustomKeyboard
という風に表示されてしまいます。そこで、Bundle nameとは別にBundle display nameをInfo.plistで定義してあげることで、思い通りの表示名にすることができます。ただ、Display nameを定義しても表示に特殊な法則性が見られたので表にしてまとめておきます。
収容アプリのDisplay name | キーボードのDisplay name | 実際の表示名 |
---|---|---|
ABCDE | ABCDE | ABCDE |
ABCDEFGHI | FGHI | FGHI |
ABCDEFGHI | CDEF | CDEF |
ABCDE FGHI | FGHI | FGHI |
ABCDE-FGHI | FGHI | FGHI |
ABCDE FGHI | FGHIJK | FGHIJK - ABCDE FGHI |
つまり、収容アプリのDisplay name名に内包する部分文字列名をキーボードのDisplay nameにした場合は、キーボードのDisplay nameのみが表示名になり、内包しない場合は、2つのDisplay nameをハイフンで繋いだ表示名になります。
ノッチあり iPhone に対応させる方法
ノッチありのキーボードは画面サイズが広くなったせいで、地球儀マークのボタンや絵文字ボタンが下の空いているスペースに勝手に表示されるようになりました。しかし、Apple によると、地球儀のボタンが2つ存在していると誤解を生んでよくないので、すでに地球儀のボタンがあるときは自前で地球儀のボタンを表示しないようにしろとのことです。地球儀のボタンを表示する必要があるかないかは、self.needsInputModeSwitchKey
で判断可能です。この値は、viewWillLayoutSubviews()
の時点で確定するようなので、ここでaddSubview()
するかしないかを決めると良さそうです。
スペルミスチェックや補完をする方法
UITextChecker とか UILexicon を使ってください。ただし、日本語には対応していませんので日本語に対応させる場合は、サードパーティ製の API を使ってください。
日本語入力をする方法
ライブラリを作りました(発展途上中)ので使ってやってください。
→ iOS 向けかな漢字変換ライブラリ:Kaede Project
Google のmozc
を使うという方法もありますが,赤ちゃんにコーヒーを飲ませるぐらい苦行なので覚悟してとりかかってください.この案件で Kishikawa 先生を知りました。
参考:Swift で Mozc for iOS を実装したいのですが、C++(ヘッダーファイル)の読み込みで躓いています。
左右端のキーの反応速度を正常にする方法
ボタンを左右端の方にレイアウトすると、時折反応が遅くなることがあります。原理はよくわかりませんが、これで正常に動くようになります。
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
}
実装不可能だと思われること
キーボードの背景を完全に透明にすること
透明なキーボードを画面全体に表示させようと試みましたが、inputView の背景を透明にしても、Layer を透明にしても、SuperView や SuperSuperView の背景を透明にしようとしても UIWindow を透明にしようとしても実現できませんでした。キーボード背景色のあのグレー色を取り除くことはできなさそうです。
挿入中の未確定文字列の背景を変化させること
self.textDocumentProxy.setMarkedText()
を使うとなんとなくそれっぽいことはできますが、挙動に難ありなのでやめた方がいいです。
参考:UITextDocumentProxy の setMarkedText を(まだ)使ってはいけない。