Swiftでシステム内蔵辞書を検索する方法

OS X 10.8 から，Mac には標準でウィズダム英和／和英辞典が内蔵されています。標準の 辞書.app や，command+control+D で検索できます。

Swift で書いている OS X 用アプリの中からウィズダム英和辞典で検索したいと思い，その方法を検討しました。

[Dictionary Services](https://developer.apple.com/library/mac/documentation/UserExperience/Reference/DictionaryServicesRef/index.html) を使えば，辞書の検索自体は可能です。
ただし，特定の辞書を指定して検索するのは，Objective-C でも Private API を使わなければできないようで，現在のところ直接的な方法は公式には用意されていないようです。

そこで，次のような方法を使うことにしました。

1. 現在の検索辞書の設定を保存する。
1. 検索辞書の設定を書き換え，ウィズダム英和辞典だけを検索辞書に指定する。
1. `DCSCopyTextDefinition` を使って辞書を検索して意味を取得する。
1. 検索辞書の設定を復元する。

このように，ユーザの検索辞書設定を一時的に変更して辞書検索を行い，すぐに元に戻すことで，特定の辞書を指定して検索を行うことが可能となります。

```swift:DictionaryServiceManager.swift
import Foundation

class DictionaryServiceManager {
    private let AppleGlobalDomainName = "Apple Global Domain"
    private let DictionaryServicesKey = "com.apple.DictionaryServices"
    private let ActiveDictionariesKey = "DCSActiveDictionaries"

    private var globalDomain: [String:Any]? { UserDefaults.standard.persistentDomain(forName: AppleGlobalDomainName) }
    private var dictionaryPreferences: [String:AnyObject]? { self.globalDomain?[DictionaryServicesKey] as! [String:AnyObject]? }
    private var currentDictionaryList: [String]? { self.dictionaryPreferences?[ActiveDictionariesKey] as! [String]? }

    private func setUserDictPreferences(_ activeDictionaries: [String]) {
        if var currentPref = self.dictionaryPreferences {
            currentPref[ActiveDictionariesKey] = activeDictionaries as AnyObject
            if var gDomain = self.globalDomain {
                gDomain[DictionaryServicesKey] = currentPref
                UserDefaults.standard.setPersistentDomain(gDomain, forName: AppleGlobalDomainName)
            }
        }
    }

    func lookUp(_ word: String, inDictionary dictionaryPath: String) -> String? {
        let currentPrefs = self.currentDictionaryList
        self.setUserDictPreferences([dictionaryPath])

        let range = CFRangeMake(0, word.utf16.count)
        let result = DCSCopyTextDefinition(nil, word as CFString, range)?.takeRetainedValue() as String?

        if let currentPrefs = currentPrefs {
            self.setUserDictPreferences(currentPrefs)
        }

        return result
    }
}
```

これを使えば，たとえば次のようにしてウィズダム英和辞典を検索できます。

```swift:DictionaryServiceManager-Test.swift
let wisdomPath: String
if #available(macOS 13, *) {
    wisdomPath = "/System/Library/AssetsV2/com_apple_MobileAsset_DictionaryServices_dictionaryOSX/c47eada3a1cc3269ef6d8120a6cc20c7dcf70355.asset/AssetData/Sanseido The WISDOM English-Japanese Japanese-English Dictionary.dictionary"
} else if #available(macOS 12, *) {
    wisdomPath = "/System/Library/AssetsV2/com_apple_MobileAsset_DictionaryServices_dictionaryOSX/b3bf6ae48c39f29f7ca1b7410a60eece625c5fc3.asset/AssetData/Sanseido The WISDOM English-Japanese Japanese-English Dictionary.dictionary"
} else if #available(macOS 11, *) {
    wisdomPath = "/System/Library/AssetsV2/com_apple_MobileAsset_DictionaryServices_dictionaryOSX/eda7c0d385d0ccb68e97b60251d63e9e2633b466.asset/AssetData/Sanseido The WISDOM English-Japanese Japanese-English Dictionary.dictionary"
} else if #available(macOS 10.15, *) {
    wisdomPath = "/System/Library/AssetsV2/com_apple_MobileAsset_DictionaryServices_dictionaryOSX/7725cba5b321a8308a603a40ac808699175c4aae.asset/AssetData/Sanseido The WISDOM English-Japanese Japanese-English Dictionary.dictionary"
} else if #available(macOS 10.14, *) {
    wisdomPath = "/System/Library/Assets/com_apple_MobileAsset_DictionaryServices_dictionaryOSX/c15aa908a684112f63017e47bb6b289e99977135.asset/AssetData/Sanseido The WISDOM English-Japanese Japanese-English Dictionary.dictionary"
} else if #available(macOS 10.13, *) {
    wisdomPath = "/System/Library/Assets/com_apple_MobileAsset_DictionaryServices_dictionaryOSX/65c5f4f22827dba6b0a6bc85755863317890953b.asset/AssetData/Sanseido The WISDOM English-Japanese Japanese-English Dictionary.dictionary"
} else {
    wisdomPath = "/Library/Dictionaries/Sanseido The WISDOM English-Japanese Japanese-English Dictionary.dictionary"
}

// "swift" の意味を取得
let word = "swift"
if let content = DictionaryServiceManager().lookUp(word, inDictionary: wisdomPath) {
    print(content)
} else {
    print("NOT FOUND")
}
```