🖼入力メソッドの設定画面
入力メソッドはシステムに組込まれるので、ユーザとやりとりするための画面を持たない。 しかし、入力メソッドの挙動を設定するための設定画面は必要である。
macOS標準の日本語入力の設定画面はキーボード設定内に組み込んでいる。
この画面を実現するための非公開APIについて説明する。
🐙ソースコード/English version
- Provide preference pane for system preferences. by mzp · Pull Request #18 · mzp/EmojiIM
- Add keyboard layout setting by mzp · Pull Request #20 · mzp/EmojiIM
- Permit read access to system preference from input method by mzp · Pull Request #21 · mzp/EmojiIM
🙊入力メソッドの設定画面
入力メソッドを選択した際、システム環境設定が入力メソッドに含まれる Resources/Preferences.prefPane
を設定画面としてロードする。
Preferences.prefPane
は、設定画面を提供するためのプラグイン形式であるPreference paneである。 Preference paneの詳細はPreference Pane Programming Guideに詳しい。
設定画面はシステム環境設定の一部としてロードされるので、直接入力メソッドとは別プロセスで動作する。 ユーザーが変更した内容は UserDefaults
などを経由して入力メソッドと共有する。
(Preference Pane Programming Guide: Figure 1 Plug-in architecture of preference panesより引用)
設定画面の追加
XcodeのPreference Paneテンプレートを用いて設定画面用のターゲットを追加する。設定画面のファイル名はPreferences.prefPaneに固定されているので、ターゲット名はPreferences
にする。
 入力メソッドをビルドする際に設定画面もビルドされるように、入力メソッドのTarget Dependenciesに設定画面を追加する。
 ビルド時に設定画面が入力メソッド内に埋め込まれるようCopy Fise Phaseを追加する。コピー先はResourcesディレクトリとする。
これでキーボード設定にて入力メソッドを選択した際に、設定画面がロードされるようになる。
🕊Swiftの利用
Preference Paneテンプレートで生成されたファイルはObjective-Cで記述されているが、Swiftで書き直したい。Swiftで書き直すために、実行時にlibswiftCore.dylibなどのSwiftの標準ライブラリをロードできるようにする必要がある。
macOSの実行ファイルは、rpathと呼ばれるディレクトリから動的リンクライブラリを探索する。 通常はこのrpathに@executable_path/../Frameworks
が含まれている。 @executable_path
は実行ファイルのパスに解決されるため、アプリケーション内のFrameworksディレクトリに依存する動的リンクライブラリを同梱する。
しかし設定ファイルはシステム環境設定に読み込まれるため、@executable_path
は /Applications/System Preferences.app/Contents/MacOS/System Preferences
に解決される。そのため、Swiftの標準ライブラリが意図した通りにロードされない。
ロードされるファイルのパスは @loader_path
で表現できる。設定画面のBuild Settingsを変更し@loader_path/../../../../Frameworks
をRunpath Search Pathsに加える。

../../../../
を用いるのは以下のように入力メソッドとFrameworksを共有するためである。

🍫CocoaPodsの利用
CocoaPodsはPreference paneに対するフレームワークの埋め込みに対応していない。そのため、podを利用したい場合は、以下のようにホストとなるアプリケーションに埋め込む必要がある。
platform :osx, '10.11' abstract_target 'App' do pod '※ikemen' <- 使える target 'EmojiIM' target 'Preferences' do pod 'NorthLayout' # <- これは使えない end end
🔑設定の共有
ユーザーが変更した設定は UserDefaults
に保存するのが一般的である。 しかし入力メソッドはサンドボックス内で実行されるため、他のアプリケーションとUserDefaultsを共有できない。サンドボックスの詳細についてはApp Sandbox Design Guideが詳しい。

(App Sandbox Design Guideより引用)
設定画面がロードされるシステム環境設定はサンドボックス外で実行されているため、設定を共有するためにはサンドボックスを越える必要がある。 サンドボックスのEntitlementsで例外設定をすることで、サンドボックス外にアクセスできるようにする。
<?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>com.apple.security.app-sandbox</key> <true/> <key>com.apple.security.temporary-exception.shared-preference.read-only</key> <string>jp.mzp.inputmethod.EmojiIM</string> </dict> </plist>
これを用いて以下のようなサンドボックスの内外で設定を共有できる UserDefaults
のラッパーを作成する。
class SettingStore { // Sandbox内のアプリケーションのbundle identifier private let kSuiteName: String = "jp.mzp.inputmethod.EmojiIM" // システム環境設定にロードされているかどうかを判定する private var inSandbox: Bool { return Bundle.main.bundleIdentifier == kSuiteName } private lazy var userDefaults: UserDefaults? = { if inSandbox { // サンドボックス内ならUserDefaults.standardを使う return UserDefaults.standard } else { // サンドボックス外なら、名前を明示的に指定する return UserDefaults(suiteName: suiteName) } }() // userDefaultsを用いて設定を書き込む func setKeyboardLayout(inputSourceID: String) { userDefaults?.set(inputSourceID, forKey: "keyboardLayout") userDefaults?.synchronize() } // userDefaultsを用いて設定を読み込む func keyboardLayout() -> String { return (userDefaults?.value(forKey: "keyboardLayout") as? String) ?? "com.apple.keylayout.US" } }