🌐各言語の入力メソッド
macOSに搭載されている各言語の入力メソッドの挙動を調べた。
🇨🇳中国語
発音を入力したのちに変換するピン音入力、注音輸入法と、漢字の字形に基づいて入力したのち変換する五筆字型入力、およびその派生型がある。いずれの方式でも、入力した文字をもとに候補を表示するため、入力メソッドとしての挙動にはそれほど違いはない。 そのためか、同一の入力メソッドの異なるモードとして実装されている。
繁体字と簡体字の2種類の字体体系があるが、入力メソッドの実装は大半が共通化されている。
入力方法とは直接の関係がないが、CapsLockを押した際の動作をカスタマイズできる。今回しらべた中では、中国語入力メソッドだけが持つ機能である。
🇯🇵日本語
入力方法についての説明は省略する。
ローマ字→かなとかな→漢字の2種類の変換に加えて、ライブ変換を持っているため、入力文字の遷移が複雑になっている。 そのためか、入力メソッドのバイナリのサイズも群を抜いて大きい。
他の言語の入力メソッドと異なり、ひらがな、カタカナといった入力モードを個別に追加・削除できない。
🇰🇷韓国語
ハングルは複数の字母の組み合わせで作られる。 その字母を順に入力していく。
「入力中テキスト」という状態を持たないため、カーソルの直前にある文字がキー操作によって変化する。
🇻🇳ベトナム語
CJKVと呼ばれるようにベトナム語には漢字(チュノム)が存在するが、日常的にはラテン文字による表記法 クオック・グーが使用される。 そのため、入力メソッドはアクセント記号の入力補助を行なう。
入力メソッドは自身が対応する文字体系をメタデータに持つが、ベトナム語入力メソッドはUnicodeに対応していると表記している。 理由はわからない。
🇮🇳ヒンディー語
ヒンディー語の表記に使われるデーヴァナーガリー文字には文字に対する記号の付与と子音結合が存在する。 そのため、カーソルの直前にある文字がキー操作によって変化する。
余談
選挙にいってきたが、漢字がうまく書けず、ベトナム語の入力方法を調べてる場合ではないのでは?? と思いました
— mzp (@mzp) 2017年10月22日
📖変換候補の表示・選択・無
絵文字入力メソッドに絵文字候補の表示・選択を追加した。やったことと、IMKCandidatesクラスのドキュメントと挙動の差異について書く。
📦コード
#8 Show candidate using IMKCandidates
📝IMKCandidatesクラスの使い方
InputMethodKitのIMKCandidatesを用いると、変換候補ウインドウを表示できる。
変換候補ウインドウ表示
show()
メソッドを用いると、変換候補ウインドウを表示できる。
let candidates: IMKCandidates = IMKCandidates( server: server, panelType: kIMKSingleColumnScrollingCandidatePanel, styleType: kIMKMain) candidates.show()
ウインドウの形状は、初期化時に渡す panelType:
引数で指定する
kIMKSingleColumnScrollingCandidatePanel
縦一列。 
kIMKSingleRowSteppingCandidatePanel
横一列。

kIMKScrollingGridCandidatePanel
グリッド状の表示。

変換候補の取得
IMKCandidates
のupdate()
を呼ぶと、 IMKInputController
の candidates(:)
kから変換候補が読み込まれる。
そのため、 入力コントローラで以下の実装を行なうとa, b, cの3つの変換候補が表示される。
open override func candidates(_ sender: Any!) -> [Any]! { return ["a", "b", "c"] }
候補ウインドウの操作
候補ウインドウは矢印キーなどで候補選択を移動できる。これは入力コントローラが受けとったキー入力イベントを候補ウインドウに渡すことで実現できる。
class EmojiInputController: IMKInputController { ... open override func handle(_ event: NSEvent!, client sender: Any!) -> Bool { if /* 候補ウインドウが表示されているならば…. */ { candidates.interpretKeyEvents([event]) return true } //別の処理 } ... }
変換候補の選択
候補選択が移動された場合や、選択が決定された場合はIMKInputController
のcandidateSelectionChanged(:)
や candidateSelected(:)
が呼ばれる。 そのため、これらのメソッドをオーバーライドすることで候補ウインドウの変化を受け取れる。
open override func candidateSelectionChanged(_ candidateString: NSAttributedString!) { // 候補選択が移動された } open override func candidateSelected(_ candidateString: NSAttributedString!) { // 候補選択が決定された }
初期状態では候補選択が決定されたのちに、候補ウインドウが非表示になる。 この動作はsetDismissesAutomatically(_:)
メソッドで変更できる。
✨絵文字入力メソッドの拡張
変換候補の表示・選択に対応するために、絵文字入力メソッドを拡張する。
- 入力中状態の間は、絵文字の候補を表示する。
- 入力中状態で←キなどの操作キーが押されたら、候補選択状態に遷移する。
- 候補が選択されたら、候補選択状態から初期状態に遷移する。
状態遷移図は以下のようになる。変換候補の表示と関係ない箇所は破線にした。

操作キー、候補選択の追加
操作キーと候補選択を表現できるよう入力の種類を追加する。
enum EventType { case input(text: String) case enter case colon case navigation // <- NEW case selected(candidate: String) // <- NEW }
操作キーはキー入力イベントとの対応を定義する。 簡単のため、アルファベット、数字、記号以外のキーを操作キーとみなす。
open class EmojiInputController: IMKInputController { ... private func convert(event: NSEvent) -> EventType? { if event.keyCode == 36 { return .enter } else if let text = event.characters { switch text { case ":": return .colon default: if !text.unicodeScalars.contains { !printable.contains($0) } { return .input(text: text) } else { return .navigation } } } else { return nil } } ... }
候補選択状態の追加
変換候補を選択中である状態を追加する。
public enum InputMethodState { case normal case composing case selection // <- NEW }
候補選択状態への遷移
入力中状態から候補選択状態への遷移を追加する。
public class EmojiAutomaton { init() { let mappings: [ActionMapping<InputMethodState, UserInput>] = [ /* Input <|> fromState => toState <|> action */ /* -------------------------------------------*/ … typeof(.navigation) <|> .composing => .selection, isSelected <|> .selection => .normal, { _ in true } <|> .selection => .selection ] ...
変換候補の取得
変換候補を検索できるよう絵文字辞書を追加する。
class EmojiDictionary { func find(prefix: String) -> [String] { return …… // prefixではじまる絵文字を返す } }
状態遷移機械に変換候補を保持するReactiveSwiftのPropertyを追加する。これは変換候補を文字列の配列として格納する。
public class EmojiAutomaton { let candidates: Property<[String]> init() { let candidatesProperty = MutableProperty<[String]>([]) self.candidates = Property(candidatesProperty) ... }
入力したテキストに応じて、変換候補が更新するよう状態遷移を変更する。
public class EmojiAutomaton { init() { let mappings: [ActionMapping<InputMethodState, UserInput>] = [ /* Input <|> fromState => toState <|> action */ /* -------------------------------------------*/ ... UserInput.isInput <|> .composing => .composing <|> { $0.ifInput { text in markedTextProperty.modify { $0.append(text) } candidatesProperty.swap(dictionary.find(prefix: text)) // <- NEW } }, UserInput.typeof(.enter) <|> .composing => .normal <|> { _ in textObserver.send(value: markedTextProperty.value) markedTextProperty.swap("") candidatesProperty.swap([]) // <- NEW }, UserInput.isSelected <|> .selection => .normal <|> { candidatesProperty.swap([]) }, ... ] ...
変換候補の更新が画面に反映するよう入力コントローラを変更する。
open class EmojiInputController: IMKInputController { public override init!(server: IMKServer!, delegate: Any!, client inputClient: Any!) { …. automaton.candidates.signal.observeValues { if $0.isEmpty { self.candidates.hide() } else { self.candidates.update() self.candidates.show() } } } open override func candidates(_ sender: Any!) -> [Any] { return automaton.candidates.value }
イベントの転送
候補選択状態の間は、キー入力を IMKCandidates
に転送する必要がある。 そのため、候補ウインドウに転送する NSEvent
を流す Signal
を追加する。
public class EmojiAutomaton { init() { let candidateEvent: Signal<NSEvent, NoError> init() { let (candidateEvent, candidateEventObserver) = Signal<NSEvent, NoError>.pipe() self.candidateEvent = candidateEvent
状態遷移のアクションを変更し、候補選択状態の場合は、この Signal
にイベントを流す。
public class EmojiAutomaton { init() { let mappings: [ActionMapping<InputMethodState, UserInput>] = [ /* Input <|> fromState => toState <|> action */ /* -------------------------------------------*/ ... , { _ in true } <|> .selection => .selection <|> { _ = $0.originalEvent.map { candidateEventObserver.send(value: $0) } } ]
入力コントローラで、このSignalを監視し、候補ウインドウに転送する。
open class EmojiInputController: IMKInputController { public override init!(server: IMKServer!, delegate: Any!, client inputClient: Any!) { ... automaton.candidateEvent.observeValues { self.candidates.interpretKeyEvents([$0]) } }
候補の選択
候補が選択された場合、その候補がテキストとして入力されるよう状態遷移を変更する。
public class EmojiAutomaton { init() { let mappings: [ActionMapping<InputMethodState, UserInput>] = [ /* Input <|> fromState => toState <|> action */ /* -------------------------------------------*/ ... isSelected <|> .selection => .normal <|> { $0.ifSelected { textObserver.send(value: $0) } markedTextProperty.swap("") candidatesProperty.swap([]) } ]
入力コントローラを変更し、候補選択時に状態遷移機械へ入力を与える。ただし、ReactvieSwiftのオペレータは再入可能ではないので、現在の遷移が完了してから入力を与えるようにする。
open class EmojiInputController: IMKInputController { open override func candidateSelected(_ candidateString: NSAttributedString!) { DispatchQueue.main.async { _ = self.automaton.handle(UserInput(eventType: .selected(candidate: candidateString.string))) } }
⚠️ 動作しないメソッド
IMKCandidates
クラスの持ついくつかのメソッドは動作しない。(macOS HighSierraで確認)
アノテーション用のメソッド:
- func showAnnotation(NSAttributedString!)
キー操作以外で候補を選択するためのメソッド郡:
- func candidateIdentifier(atLineNumber: Int)
- func candidateStringIdentifier(Any!)
- func lineNumberForCandidate(withIdentifier: Int)
- func selectCandidate(Int)
- func selectCandidate(withIdentifier: Int)
- func clearSelection()
その他:
- func attachChild(IMKCandidates!, toCandidate: Int, type: IMKStyleType)
- func showChild()
- func showSublist([Any]!, subListDelegate: Any!)
- func detachChild(Int)
- func hideChild()
詳細は(rdar://34944196およびrdar://34911503)に書いたが、これらのメソッドはなにもしない、もしくは定数を返すメソッドとして実装されている。 無である。
🚀bitrise.io for macOS app
macOSアプリのCIとしてbitrise.ioを使いはじめた。
❌ 署名エラー
初期状態でテストを実行すると、アプリケーションに署名するための証明書がbitrise.ioに登録されていないため、エラーとなる。
❌ error: No signing certificate "Mac Development" found: No "Mac Development" signing certificate matching team ID "XXXX" with a private key was found. (in target 'Tests')
✨証明書の設定
デバッグビルドで利用する Mac Developer証明書は、開発用のデバイスで動かすための証明書なので、CI上で利用できない。 そこで、Mac App Store以外でアプリケーションを配布するための Developer ID Application証明書を利用する。 (参考: Managing Your Developer Account Team)
具体的にやったことはhttps://github.com/mzp/EmojiIM/pull/5にまとめてある。
手順1: "Developer ID Application"で署名する。
automatic signingを無効にし、"Developer ID Application"証明書で署名するよう設定する。同じ設定をテスト用のターゲットに対しても行なう。
手順2: 秘密鍵をbitriseにアップロードする。
XcodeのAccountsタブから、"Developer ID Application"証明書をエクスポートする。
パスワードを設定する。
エクスポートされたp12ファイルをワークフローエディタからアップロードする。エクスポート時に指定したパスワードもここで指定する。
手順3: リリースビルドでテストする
リリースビルドに対してテストを行なうようScanfileで設定する。
workspace 'EmojiIM.xcworkspace' scheme 'EmojiIM' configuration 'Release'
手順4: Bitriseでfastlaneを使う
fastlane tools integration - Bitrise DevCenterにあるようfastlane stepをworkflowに追加する。
🏷 その他
"code object is not signed at all" errorエラーの回避
2回に1回くらい codesign
コマンドが"code object is not signed at all"というエラーで失敗する。 詳細は分からないがStack Overflowにあるように--deep
フラグを渡したら解決した。
デバッグ時にautomatic signingを使う
Build settingsの Code signing style
から指定すれば、デバッグ時にのみautomatic signingを使える。
💥様子
30回くらい失敗を連続させた。
⚡️ReactiveInputMethod
🍣入力メソッドを拡張し、テキストを入力し、Enterで確定できるようにした。
コード
https://github.com/mzp/EmojiIM/tree/marked
未確定文字列の挿入
未確定文字列は、入力セッションの一部としてマークされている文字列なのでmarked textと呼ばれる。
IMKTextInput
プロトコルの setMarkedText
で設定できる。例えば"あいうえ"を未確定文字列として表示したい場合は、以下のコードになる。
let notFound = NSRange(location: NSNotFound, length: NSNotFound) client.setMarkedText("あいうえ", selectionRange: notFound, replacementRange: notFound)
クリアしたい場合は第一引数に空文字列を渡す。nilを渡すとクラッシュする。
状態遷移
未確定文字列の入力した後、入力を確定するためには、入力メソッド内で状態遷移を管理する必要がある。
未確定文字列を持たない初期状態と、未確定文字列を持つ入力中状態があり、キー入力とEnterキーの入力で遷移するので 状態遷移図は以下のようになる。
ReactiveAutomaton
状態遷移を記述するためにReactiveAutomatonを用いる。
これはReactiveCocoaで状態遷移機械を書くためのライブラリである。アイデアは以下のスライドで解説されいる。
このライブラリを使い、入力メソッドの状態遷移および遷移時のアクションを定義する。
入力
ユーザの入力はテキスト入力とEnterの押下の2種類とする。 以下のenumで定義する。テキスト入力は入力された文字列をパラメータに持つようにする。
public enum UserInput { case input(text: String) case enter }
状態
入力メソッドの状態は「通常状態」と「入力中状態」の2種類とする。以下のenumで定義する。
public enum InputMethodState { case normal case composing }
遷移
ReactiveAutomatonのDSLを用いて状態遷移を定義する。
static func isInput(_ state: UserInput) -> Bool { switch state { case .input: return true default: return false } } let mappings: [ActionMapping<InputMethodState, UserInput>] = [ /* Input <|> fromState => toState */ /* --------------------------------*/ isInput <|> .normal => .composing, isInput <|> .composing => .composing, .enter <|> .composing => .normal ]
遷移が発生した際に実行するアクションを指定できるようにDSLを拡張する。 (ReactiveAutomaton+Action.swift) そして、遷移時のアクションで確定文字列、未確定文字列を更新する。
let (text, textObserver) = Signal<String, NoError>.pipe() let markedTextProperty = MutableProperty<String>("") let mappings: [ActionMapping<InputMethodState, UserInput>] = [ /* Input <|> fromState => toState <|> action */ /* -------------------------------------------*/ isInput <|> .normal => .composing <|> { switch $0 { case .input(text: let text): markedTextProperty.swap(text) default: () } }, isInput <|> .composing => .composing <|> { switch $0 { case .input(text: let text): markedTextProperty.modify { $0.append(text) } default: () } }, .enter <|> .composing => .normal <|> { _ in textObserver.send(value: markedTextProperty.value) markedTextProperty.swap("") } ]
この状態遷移をもとに、オートマトンを作る。
let (inputSignal, observer) = Signal<UserInput, NoError>.pipe() self.automaton = Automaton(state: .normal, input: inputSignal, mapping: reduce(mappings))
入力コントローラとの接続
イベントの処理
オートマトンと入力コントローラを接続するために、オートマトンにイベントを送るメソッドを作る。 入力コントローラの仕様にあわせ、状態遷移が発生した場合は真を、そうでない場合は偽を返すようにする。
init() { ... automaton.replies.observeValues { switch $0 { case .success: self.handled = true default: () } } } func handle(_ input: UserInput) -> Bool { handled = false observer.send(value: input) return handled }
入力コントローラで、キー入力に応じて、このメソッドを呼びだす。
public override func handle(_ event: NSEvent!, client sender: Any!) -> Bool { if event.keyCode == 36 { return automaton.handle(.enter) } else if event.keyCode == 51 { return automaton.handle(.backspace) } else if let text = event.characters { return automaton.handle(.input(text: text)) } else { return false } }
未確定文字列・確定文字列の反映
オートマトンが持つ入力文字列、未確定文字列を監視し、更新があった際にクライアントアプリケーションに反映するようにする。これはReaciveSwiftのイベント監視の仕組みを用いる。
public override init!(server: IMKServer!, delegate: Any!, client inputClient: Any!) { super.init(server: server, delegate: delegate, client: inputClient) guard let client = inputClient as? IMKTextInput else { return } // 未確定文字列の反映 automaton.markedText.signal.observeValues { text in let notFound = NSRange(location: NSNotFound, length: NSNotFound) client.setMarkedText(text, selectionRange: notFound, replacementRange: notFound) } // 確定文字列の挿入 automaton.text.observeValues { let notFound = NSRange(location: NSNotFound, length: NSNotFound) client.insertText($0, replacementRange: notFound) } }
🔬Appleの非公開APIの調べ方
Touchbarに変換候補を出すの、とうとう成功した pic.twitter.com/zXNjawZct5
— mzp (@mzp) 2017年9月30日
入力メソッドからタッチバーを使う方法を調べるために使った各種ツールの使い方をメモしておく。
class-dump
実行ファイルからObjective-Cのヘッダファイルを生成する。 homebrewでインストールできた。
$ class-dump /System/Library/Frameworks/InputMethodKit.framework/InputMethodKit // // Generated by class-dump 3.5 (64 bit). // // class-dump is Copyright (C) 1997-1998, 2000-2001, 2004-2015 by Steve Nygard. // ..... @interface IMKServer : NSObject <IMKServerProxy> { IMKServerPrivate *_private; } + (id)inputDelegateClassNameFor:(id)arg1; + (id)inputControllerClassNameFor:(id)arg1; + (id)connectionNameFor:(id)arg1; + (id)_clientWrapperForXPCConn:(id)arg1; + (id)_clientWrapperForDOProxy:(id)arg1; + (id)imkServerSingleton; - (id)keyBindingManager; ...
lldb
class-dumpでは、だいたいの箇所がid型になってて詳細な使い型がわからない。 そのライブラリを使っていそうなプロセスにlldbでattachして使い方を調べた。
まずはSIPが有効だとプロセスにattachできないので、Mac OS X El CapiptanでSIPを無効化する - Qiitaなどを参考に無効化する。
目的のプロセスを調べて、attachする。 入力メソッドにattachする場合はそのマシンの入力が死ぬので、別マシンからsshするといい。
$ ps ax | grep '[E]mojiFunctionRowIM' 402 ?? Ss 0:03.51 /System/Library/Input Methods/EmojiFunctionRowIM.app/Contents/PlugIns/EmojiFunctionRowIM_Extension.appex/Contents/MacOS/EmojiFunctionRowIM_Extension $ lldb (lldb) attach 402
あとは各種コマンドを使って挙動を調べていく。 だいたい以下のコマンドを使った。
ブレイクポイントの設定
特定のセレクタにブレイクポイントを設定する。
br set --selector inputDelegateClassNameFor:
特定のクラスの全メソッドにブレイクポイントを設定する。
br set -r '\[ClassName .*\]$'
表示
# オブジェクトの表示 po $obj # 値の表示 p (BOOL)[$obj isHoge]
メソッド呼び出し時に使われるレジスタ
- self: $rdi
- _cmd $rsi
引数
変数への代入
expr id $foo = [$obj foo]
otool
それでも分からないやつは逆アセンブルした。 どのメソッド呼んでいるかなどを主に見た。
$ otool -tV /System/Library/Frameworks/InputMethodKit.framework/InputMethodKit /System/Library/Frameworks/InputMethodKit.framework/InputMethodKit: (__TEXT,__text) section -[IMKUICandidateController init]: 0000000000001d0c pushq %rbp 0000000000001d0d movq %rsp, %rbp 0000000000001d10 pushq %r15 0000000000001d12 pushq %r14 0000000000001d14 pushq %r12 0000000000001d16 pushq %rbx 0000000000001d17 subq $0x10, %rsp 0000000000001d1b leaq -0x30(%rbp), %rax 0000000000001d1f movq %rdi, (%rax) 0000000000001d22 movq 0xd6fc7(%rip), %rcx ## Objc class ref: IMKUICandidateController 0000000000001d29 movq %rcx, 0x8(%rax) 0000000000001d2d movq 0xd2dc4(%rip), %r14 ## Objc selector ref: init 0000000000001d34 movq %rax, %rdi 0000000000001d37 movq %r14, %rsi 0000000000001d3a callq 0x89b5c ## Objc message: -[[%rdi super] init] 0000000000001d3f movq %rax, %rbx 0000000000001d42 testq %rbx, %rbx 0000000000001d45 je 0x1de9 ....
🐙Githubブラウザ for iOS11
iOS11で追加されたFilesアプリから、Githubレポジトリを見るための拡張を作った。 AppStoreから入手できる。
Github->Octocat->Octopusという連想で、OctoEyeという名前にした。
⭐️使い方
OctoEyeをインストール・初期設定をすると、FilesのLocationsにGithubが追加される。
ここからレポジトリの内容をできる。
さらに、Textasticといった別アプリから開くこともできる。 ただし、保存はサポートしていない。
📦ソースコード
🎨デザイン
Sketch
最近Sketchを買ったので、事前にデザインを描いた。
今までは適当な紙に落書きするだけだったが、Sketchで描くと考慮漏れに気付けたり、単純に楽しかったりしてよかった。
アイコン
アイコンは最初いらすと屋のタコを使い、途中で自分で描いたのに差し替えた。
GithubなのでOctocatを使いたかったが、そのものを使うわけにはいかないので、一部だけ+シルエットにした。 猫耳だけを拡大したバージョンも作ったが今ひとつだったので段ボールバージョンを採用した。
ウォークスルー
初回起動時に出すウォークスルーを作った。 これは結構大変だった。
EAIntroViewの動きの確認。
アプリ起動直後にでてくるウォークスルーを作る練習してるけど、かわいい画像を挟んだら急に体験の質が向上した。 pic.twitter.com/TxG5HG28jJ
— mzp (@mzp) 2017年8月5日
iPadProで下書きをした。
雰囲気が分かったのでSketchでいろいろ描いて、デザインを検討した。
実装した。
🛠開発
Github Graphql
Githubから情報を取得するために、GraphqlAPIを利用している。 FileProviderExtensionではネットワークアクセスの回数を減らしたかったのでちょうどよかった。
ただバイナリデータを取得することができなかったので、そこだけはREST APIを使っている。
ReactiveCocoa/ReactiveSwift
レポジトリ追加画面などではReactiveSwiftを使っている。最初はRxSwiftを使おうとしたが、@banjunにReactiveSwiftのほうが語彙がSwiftっぽいと言われたので、ReactiveSwiftにした。
アイマスライブの合間にReactiveSwiftを教えてもらってたけど、プロデューサーの発音に関する話がよかった https://t.co/Xg9Hh6uJE9
— mzp (@mzp) 2017年7月31日
界隈の流行りに乗ってプロダクトに入れてみようと今週はReactiveSwiftやってたんだけど,Producer=「プロデューサー」,SignalProducer=「シグナルプロデュー↑サー↓」,小日向美穂「プロデュー↑サー↓」という知見を得た
— ばんじゅん🍓 (@banjun) 2017年5月24日
イベントをmap等で加工するのは分かるが、画面上の要素とバインドする部分がよく分かんないまま書いていた。
スクリーンショット
AppStoreに提出用のスクリーンショットはfastlaneのsnapshotとframeitで生成した。
snapshotで各デバイス用のスクリーンショットを準備し、frameitでiPhone/iPadの枠をつけ上部に文字を入れた。
Xcode9からUITestが複数アプリに対応したので、Filesアプリの撮影も自動化できた。(がMetadata Rejectになったので、このスクショは削除した)
その他
- CIはTravisCIを使った。 Xcodeの新しいベータが出て3〜4日でTravis側も更新されててすごかった。
- ベータ版のOSにはスクリーンショットを外部に出せない制約が付くので、プルリクエストに画像を貼れなかった。
🚀今後の予定
- Filesにあるタグ機能やお気に入り機能をサポートしてないのでやりたい。
- masterブランチ以外は見れないので、切り替え機能をつけたい。
- 書き込み機能もつけたいが、コミットメッセージをどうするかが難しいと思っている。
🍣入力メソッド
すべてのキー入力に対して🍣を入力する入力メソッドを作った。
🍣を入力するだけのInputMethodができた pic.twitter.com/0If6KVNTAT
— mzp (@mzp) 2017年9月16日
コード
https://github.com/mzp/EmojiIM
入力メソッドの構成
入力メソッドによるテキスト入力は、各アプリケーションと入力メソッドが通信することで実現されている。アプリケーションはキー入力を入力メソッドに送信し、入力メソッドがアプリケーションに対してテキスト入力を行なう。
この通信のために入力メソッドはIMKServerクラスを用いてサーバーを起動する。そして各アプリケーションはクライアントとなり、サーバーと通信する。この通信を入力セッション(input session)と呼ぶ。
サーバーはクライアントごとに入力コントローラーを生成する。この入力コントローラーがキー入力からテキスト入力を生成する。
プロジェクトの作成
Xcodeでプロジェクトを作成する。Cocoa Appテンプレートを選ぶ。 Product nameはなんでもいいが、Bundle identifierにinputmethodという文字列を含める必要がある。

起動時の処理
入力メソッドが起動時に各アプリケーションと通信するための IMKServer
を起動する。このとき、クライアントがサーバーに接続する際に使う名前を指定する。
import Cocoa import InputMethodKit private var server: IMKServer? @NSApplicationMain internal class AppDelegate: NSObject, NSApplicationDelegate { @IBOutlet private weak var window: NSWindow! func applicationDidFinishLaunching(_ aNotification: Notification) { server = IMKServer(name: "EmojiInputSession", bundleIdentifier: Bundle.main.bundleIdentifier) } }
入力コントローラ
入力コントローラとして使うクラスを定義する。このクラスは IMKInputController
の派生クラスにする必要がある。
このクラスの名前をInfo.plistで指定す。そのため、objc
属性を用いて、Swiftコンパイラによってマングリングされないようにする。
@objc(EmojiInputController) open class EmojiInputController: IMKInputController {
キー入力時に inputText
メソッドが呼ばれる。キー入力を処理したかどうかを真偽値で返す。
func inputText(_ string: String!, client sender: Any!) -> Bool
テキスト入力をしたいアプリケーションは client
引数として渡される。 ただし、 Any
型のままではテキスト入力を行なえないのでIMKTextInput
プロトコルにキャストする。 キャストに失敗した場合は、入力メソッドが処理しなかったことを示すために偽を返す。
guard let client = sender as? IMKTextInput else { return false }
IMKTextInput
の insertText
メソッドを用いて、入力したい文字列を挿入する。
client.insertText("🍣", replacementRange: NSRange(location: NSNotFound, length: NSNotFound))
Info.plist
Info.plistで入力セッションに使う名前や入力コントローラとして使うクラスの名前を指定する。
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> <dict> <key>CFBundleDevelopmentRegion</key> <string>$(DEVELOPMENT_LANGUAGE)</string> <key>CFBundleExecutable</key> <string>$(EXECUTABLE_NAME)</string> <key>CFBundleIconFile</key> <string></string> <key>CFBundleIdentifier</key> <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string> <key>CFBundleInfoDictionaryVersion</key> <string>6.0</string> <key>CFBundleName</key> <string>$(PRODUCT_NAME)</string> <key>CFBundlePackageType</key> <string>APPL</string> <key>CFBundleShortVersionString</key> <string>1.0</string> <key>CFBundleVersion</key> <string>1</string> <key>LSBackgroundOnly</key> <string>YES</string> <key>LSHasLocalizedDisplayName</key> <false/> <key>LSMinimumSystemVersion</key> <string>$(MACOSX_DEPLOYMENT_TARGET)</string> <key>NSHumanReadableCopyright</key> <string>Copyright © 2017 mzp. All rights reserved.</string> <key>NSMainNibFile</key> <string>MainMenu</string> <key>NSPrincipalClass</key> <string>NSApplication</string> <!-- 入力セッションに使う名前 --> <key>InputMethodConnectionName</key> <string>EmojiInputSession</string> <!-- 入力コントローラのクラス名 --> <key>InputMethodServerControllerClass</key> <string>EmojiInputController</string> <!-- 入力メソッドのアイコンとして使う画像 --> <key>tsInputMethodIconFileKey</key> <string>InputMethodIcon.tiff</string> <!-- 入力ソース(入力ソースの詳細はよくわからん) --> <key>TISIntendedLanguage</key> <string>ja</string> <!-- 入力ソースに対する識別子 --> <key>TISInputSourceID</key> <string>jp.mzp.inputmethod.EmojiIM</string> <!-- 入力メソッドが対応している言語のリスト --> <key>tsInputMethodCharacterRepertoireKey</key> <array> <string>Hira</string> <string>Kana</string> <string>Latn</string> </array> </dict> </plist>
インストール
ビルドして生成された EmojiIM.app
を ~/Library/Input Methods
にコピーする。Workspace settingsからDerivedDataをworkspace relativeにしておくと楽。 こんな感じでコピーする。
cp -r DerivedData/EmojiIM/Build/Products/Debug/EmojiIM.app ~/Library/Input\\ Methods
System PreferencesのInput Sourcesから追加できる。うまく反映されない場合は、いったんログアウトするとよい。