From 5369f36df9459a271d9972fe8b3bbe76fede6659 Mon Sep 17 00:00:00 2001 From: mtgto Date: Mon, 28 Oct 2024 00:02:16 +0900 Subject: [PATCH 1/7] =?UTF-8?q?FileDict,=20UserDict=E3=82=92MainActor?= =?UTF-8?q?=E3=81=AB=E6=BA=96=E6=8B=A0=E3=81=95=E3=81=9B=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- macSKK/Dict.swift | 2 +- macSKK/FileDict.swift | 59 ++++++++----------------- macSKK/MemoryDict.swift | 2 +- macSKK/Settings/SettingsViewModel.swift | 4 +- macSKK/UserDict.swift | 18 ++++---- 5 files changed, 33 insertions(+), 52 deletions(-) diff --git a/macSKK/Dict.swift b/macSKK/Dict.swift index 2bc915fb..b19464d6 100644 --- a/macSKK/Dict.swift +++ b/macSKK/Dict.swift @@ -29,7 +29,7 @@ struct DictLoadEvent { let status: DictLoadStatus } -protocol DictProtocol { +@MainActor protocol DictProtocol { /** * 辞書を引き変換候補順に返す * diff --git a/macSKK/FileDict.swift b/macSKK/FileDict.swift index 6ee017f0..c11fa425 100644 --- a/macSKK/FileDict.swift +++ b/macSKK/FileDict.swift @@ -29,14 +29,13 @@ enum FileDictType: Equatable { } /// 実ファイルをもつSKK辞書 -class FileDict: NSObject, DictProtocol, Identifiable { +@MainActor class FileDict: NSObject, DictProtocol, Identifiable { // FIXME: URLResourceのfileResourceIdentifierKeyをidとして使ってもいいかもしれない。 // FIXME: ただしこの値は再起動したら同一性が保証されなくなるのでIDとしての永続化はできない // FIXME: iCloud Documentsとかでてくるとディレクトリが複数になるけど、ひとまずファイル名だけもっておけばよさそう。 let id: String let fileURL: URL let type: FileDictType - private var version: NSFileVersion? /// ファイルの書き込み・読み込みを直列で実行するためのキュー private let fileOperationQueue = { let queue = OperationQueue() @@ -71,7 +70,7 @@ class FileDict: NSObject, DictProtocol, Identifiable { } // MARK: NSFilePresenter - var presentedItemURL: URL? { fileURL } + nonisolated var presentedItemURL: URL? { fileURL } let presentedItemOperationQueue: OperationQueue = { let queue = OperationQueue() queue.maxConcurrentOperationCount = 1 @@ -84,14 +83,13 @@ class FileDict: NSObject, DictProtocol, Identifiable { self.fileURL = fileURL self.type = type self.dict = MemoryDict(entries: [:], readonly: readonly) - self.version = NSFileVersion.currentVersionOfItem(at: fileURL) self.readonly = readonly super.init() - load() + load(fileURL: fileURL) NSFileCoordinator.addFilePresenter(self) } - func load() { + nonisolated func load(fileURL: URL) { let operation = BlockOperation { var coordinationError: NSError? var readingError: NSError? @@ -99,9 +97,10 @@ class FileDict: NSObject, DictProtocol, Identifiable { NotificationCenter.default.post(name: notificationNameDictLoad, object: DictLoadEvent(id: self.id, status: .loading)) - fileCoordinator.coordinate(readingItemAt: self.fileURL, error: &coordinationError) { [weak self] url in + fileCoordinator.coordinate(readingItemAt: fileURL, error: &coordinationError) { [weak self] url in if let self { do { + let memoryDict: MemoryDict if case .json = self.type { let decoder = JSONDecoder() decoder.keyDecodingStrategy = .convertFromSnakeCase @@ -116,24 +115,24 @@ class FileDict: NSObject, DictProtocol, Identifiable { let okuriNashiEntries = jisyo.okuriNasi.mapValues({ $0.map { Word($0) } }) - let memoryDict = MemoryDict(okuriAriEntries: okuriAriEntries, okuriNashiEntries: okuriNashiEntries, readonly: readonly) - self.dict = memoryDict + memoryDict = MemoryDict(okuriAriEntries: okuriAriEntries, okuriNashiEntries: okuriNashiEntries, readonly: readonly) } else if case .traditional(let encoding) = self.type { let source = try self.loadString(url, encoding: encoding) if source.isEmpty { // 辞書ファイルを書き込み中に読み込んでしまった? logger.warning("辞書 \(self.id) を読み込んだところ0バイトだったため更新を無視します") } - let memoryDict = MemoryDict(dictId: self.id, source: source, readonly: readonly) + memoryDict = MemoryDict(dictId: self.id, source: source, readonly: readonly) + } else { + fatalError() + } + Task { @MainActor in self.dict = memoryDict } - self.version = NSFileVersion.currentVersionOfItem(at: url) - logger.log("辞書 \(self.id, privacy: .public) から \(self.dict.entries.count) エントリ読み込みました") NotificationCenter.default.post(name: notificationNameDictLoad, object: DictLoadEvent(id: self.id, - status: .loaded(success: dict.entryCount, failure: dict.failedEntryCount))) + status: .loaded(success: memoryDict.entryCount, failure: memoryDict.failedEntryCount))) } catch { - logger.error("辞書 \(self.id, privacy: .public) の読み込みでエラーが発生しました: \(error)") NotificationCenter.default.post(name: notificationNameDictLoad, object: DictLoadEvent(id: self.id, status: .fail(error))) @@ -149,7 +148,7 @@ class FileDict: NSObject, DictProtocol, Identifiable { operation.waitUntilFinished() } - private func loadString(_ url: URL, encoding: String.Encoding) throws -> String { + nonisolated private func loadString(_ url: URL, encoding: String.Encoding) throws -> String { if encoding == .japaneseEUC { let data = try Data(contentsOf: url) return try data.eucJis2004String() @@ -172,13 +171,13 @@ class FileDict: NSObject, DictProtocol, Identifiable { return try String(contentsOf: url, encoding: encoding) } - func save() { + func save(to url: URL, encoding: String.Encoding) { if !hasUnsavedChanges { logger.log("辞書 \(self.id, privacy: .public) は変更されていないため保存は行いません") return } let operation = BlockOperation { - guard let data = self.serialize().data(using: self.type.encoding) else { + guard let data = self.serialize().data(using: encoding) else { fatalError("辞書 \(self.id) のシリアライズに失敗しました") } var coordinationError: NSError? @@ -187,7 +186,6 @@ class FileDict: NSObject, DictProtocol, Identifiable { fileCoordinator.coordinate(writingItemAt: self.fileURL, error: &coordinationError) { [weak self] newURL in if let self { do { - self.version = try NSFileVersion.addOfItem(at: newURL, withContentsOf: newURL) logger.log("辞書のバージョンを作成しました") } catch { logger.error("辞書のバージョン作成でエラーが発生しました: \(error)") @@ -285,35 +283,16 @@ class FileDict: NSObject, DictProtocol, Identifiable { } extension FileDict: NSFilePresenter { - // 他プログラムでの書き込みなどでは呼ばれないみたい - func presentedItemDidGain(_ version: NSFileVersion) { - if version == self.version { - logger.log("辞書 \(self.id, privacy: .public) のバージョンが自分自身に更新されたため何もしません") - } else { - logger.log("辞書 \(self.id, privacy: .public) のバージョンが更新されたので読み込みます") - load() - } - } - - func presentedItemDidLose(_ version: NSFileVersion) { - logger.log("辞書 \(self.id, privacy: .public) が更新されたので読み込みます (バージョン情報が消失)") - load() - } - // NOTE: save() で保存した場合はバージョンが必ず更新されるのでこのメソッドは呼ばれない // IMEとして動いているmacSKK (A) とXcodeからデバッグ起動しているmacSKK (B) の両方がいる場合、 // どちらも同じ辞書ファイルを監視しているので、Aが保存してもAのpresentedItemDidChangeは呼び出されないが、 // BのpresentedItemDidChangeは呼び出される。 - func presentedItemDidChange() { + nonisolated func presentedItemDidChange() { guard let version = NSFileVersion.currentVersionOfItem(at: fileURL) else { logger.error("辞書 \(self.id, privacy: .public) のバージョンが存在しません") return } - if version == self.version { - logger.log("辞書 \(self.id, privacy: .public) がアプリ外で変更されたため読み込みます") - } else { - logger.log("辞書 \(self.id, privacy: .public) が変更されたので読み込みます") - } - load() + logger.log("辞書 \(self.id, privacy: .public) が変更されたので読み込みます") + load(fileURL: self.fileURL) } } diff --git a/macSKK/MemoryDict.swift b/macSKK/MemoryDict.swift index 81d4482a..b26cf99e 100644 --- a/macSKK/MemoryDict.swift +++ b/macSKK/MemoryDict.swift @@ -4,7 +4,7 @@ import Foundation /// 実ファイルをもたないSKK辞書 -struct MemoryDict: DictProtocol { +struct MemoryDict: DictProtocol, Sendable { /** * 読み込み専用で保存しないかどうか * diff --git a/macSKK/Settings/SettingsViewModel.swift b/macSKK/Settings/SettingsViewModel.swift index 0e61d0f0..36c6ea3e 100644 --- a/macSKK/Settings/SettingsViewModel.swift +++ b/macSKK/Settings/SettingsViewModel.swift @@ -488,8 +488,10 @@ final class SettingsViewModel: ObservableObject { if let userDict = Global.dictionary.userDict as? FileDict, userDict.id == loadEvent.id { self.userDictLoadingStatus = loadEvent.status if case .fail(let error) = loadEvent.status { + logger.error("辞書 \(loadEvent.id, privacy: .public) の読み込みでエラーが発生しました: \(error)") UNNotifier.sendNotificationForUserDict(readError: error) - } else if case .loaded(_, let failureCount) = loadEvent.status, failureCount > 0 { + } else if case .loaded(let successCount, let failureCount) = loadEvent.status, failureCount > 0 { + logger.log("辞書 \(loadEvent.id, privacy: .public) から \(successCount) エントリ読み込みました") UNNotifier.sendNotificationForUserDict(failureEntryCount: failureCount) } } else { diff --git a/macSKK/UserDict.swift b/macSKK/UserDict.swift index f03e04b8..89f4fa6e 100644 --- a/macSKK/UserDict.swift +++ b/macSKK/UserDict.swift @@ -8,8 +8,8 @@ import Foundation /// v0.22.0以降はskkservサーバーを辞書としても利用することが可能。 /// /// TODO: ファイル辞書にしかない単語を削除しようとしたときにどうやってそれを記録するか。NG登録? -class UserDict: NSObject, DictProtocol { - static let userDictFilename = "skk-jisyo.utf8" +@MainActor class UserDict: NSObject, DictProtocol { + nonisolated static let userDictFilename = "skk-jisyo.utf8" let dictionariesDirectoryURL: URL let userDictFileURL: URL /** @@ -68,9 +68,9 @@ class UserDict: NSObject, DictProtocol { // 短期間に複数の保存要求があっても60秒に一回にまとめる .debounce(for: .seconds(60), scheduler: DispatchQueue.global(qos: .background)) .sink { [weak self] _ in - if let fileDict = self?.userDict as? FileDict { + if let self, let fileDict = self.userDict as? FileDict { logger.log("ユーザー辞書を永続化します。現在のエントリ数は \(fileDict.dict.entries.count)") - fileDict.save() + fileDict.save(to: fileDict.fileURL, encoding: fileDict.type.encoding) } } .store(in: &cancellables) @@ -255,7 +255,7 @@ class UserDict: NSObject, DictProtocol { } if let userDict { if let dict = userDict as? FileDict { - dict.save() + dict.save(to: dict.fileURL, encoding: dict.type.encoding) } else { // ユニットテストなど特殊な場合のみ logger.info("永続化が要求されましたが、ユーザー辞書がファイル形式でないため無視されます") @@ -278,7 +278,7 @@ class UserDict: NSObject, DictProtocol { } extension UserDict: NSFilePresenter { - func presentedSubitemDidAppear(at url: URL) { + nonisolated func presentedSubitemDidAppear(at url: URL) { do { if try isValidFile(url) { logger.log("新しいファイル \(url.lastPathComponent, privacy: .public) が作成されました") @@ -293,7 +293,7 @@ extension UserDict: NSFilePresenter { } // 他フォルダから移動された場合だけでなく他フォルダに移動した場合にも発生する (後者はdidMoveToも発生する) - func presentedSubitemDidChange(at url: URL) { + nonisolated func presentedSubitemDidChange(at url: URL) { // 削除されたときにaccommodatePresentedSubitemDeletionが呼ばれないがこのメソッドは呼ばれるようだった。 // そのためこのメソッドで削除のとき同様の処理を行う。 if !FileManager.default.fileExists(atPath: url.path) { @@ -326,7 +326,7 @@ extension UserDict: NSFilePresenter { } // 子要素を他フォルダに移動した場合に発生する - func presentedSubitem(at oldURL: URL, didMoveTo newURL: URL) { + nonisolated func presentedSubitem(at oldURL: URL, didMoveTo newURL: URL) { logger.log("ファイル \(oldURL.lastPathComponent, privacy: .public) が辞書フォルダから移動されました") NotificationCenter.default.post(name: notificationNameDictFileDidMove, object: oldURL) } @@ -339,7 +339,7 @@ extension UserDict: NSFilePresenter { } // 辞書ファイルとして問題があるファイルでないかを判定する - private func isValidFile(_ fileURL: URL) throws -> Bool { + nonisolated private func isValidFile(_ fileURL: URL) throws -> Bool { return try fileURL.isReadable() } } From 14f8746128af20dd1a2e6ed9cff066e994c2727a Mon Sep 17 00:00:00 2001 From: mtgto Date: Sat, 2 Nov 2024 10:54:36 +0900 Subject: [PATCH 2/7] =?UTF-8?q?FileDict=E3=81=AENSOperationQueue=E5=AE=9F?= =?UTF-8?q?=E8=A1=8C=E3=82=92=E5=89=8A=E9=99=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- macSKK/FileDict.swift | 47 +++++++++++++++++-------------------------- 1 file changed, 18 insertions(+), 29 deletions(-) diff --git a/macSKK/FileDict.swift b/macSKK/FileDict.swift index c11fa425..321e0b64 100644 --- a/macSKK/FileDict.swift +++ b/macSKK/FileDict.swift @@ -176,38 +176,27 @@ enum FileDictType: Equatable { logger.log("辞書 \(self.id, privacy: .public) は変更されていないため保存は行いません") return } - let operation = BlockOperation { - guard let data = self.serialize().data(using: encoding) else { - fatalError("辞書 \(self.id) のシリアライズに失敗しました") - } - var coordinationError: NSError? - var writingError: NSError? - let fileCoordinator = NSFileCoordinator(filePresenter: self) - fileCoordinator.coordinate(writingItemAt: self.fileURL, error: &coordinationError) { [weak self] newURL in - if let self { - do { - logger.log("辞書のバージョンを作成しました") - } catch { - logger.error("辞書のバージョン作成でエラーが発生しました: \(error)") - writingError = error as NSError - return - } - do { - try data.write(to: newURL) - self.hasUnsavedChanges = false - logger.log("辞書を永続化しました。現在のエントリ数は \(dict.entries.count)、シリアライズ後のファイルサイズは\(data.count)バイトです") - } catch { - logger.error("辞書 \(self.id, privacy: .public) の書き込みに失敗しました: \(error)") - writingError = error as NSError - } + guard let data = self.serialize().data(using: encoding) else { + fatalError("辞書 \(self.id) のシリアライズに失敗しました") + } + var coordinationError: NSError? + var writingError: NSError? + let fileCoordinator = NSFileCoordinator(filePresenter: self) + fileCoordinator.coordinate(writingItemAt: self.fileURL, error: &coordinationError) { [weak self] newURL in + if let self { + do { + try data.write(to: newURL) + self.hasUnsavedChanges = false + logger.log("辞書を永続化しました。現在のエントリ数は \(dict.entries.count)、シリアライズ後のファイルサイズは\(data.count)バイトです") + } catch { + logger.error("辞書 \(self.id, privacy: .public) の書き込みに失敗しました: \(error)") + writingError = error as NSError } } - if let error = coordinationError ?? writingError { - logger.error("辞書 \(self.id, privacy: .public) の読み込み中にエラーが発生しました: \(error)") - } } - fileOperationQueue.addOperation(operation) - operation.waitUntilFinished() + if let error = coordinationError ?? writingError { + logger.error("辞書 \(self.id, privacy: .public) の読み込み中にエラーが発生しました: \(error)") + } } deinit { From 0f1d08aa3b0491ea1e1cbc8523185c93c60dd321 Mon Sep 17 00:00:00 2001 From: mtgto Date: Sat, 28 Dec 2024 21:01:47 +0900 Subject: [PATCH 3/7] =?UTF-8?q?MemoryDict=E3=81=AF`@MainActor`=E5=88=86?= =?UTF-8?q?=E9=9B=A2=E3=81=8C=E4=B8=8D=E8=A6=81=E3=81=AA=E3=81=AE=E3=81=A7?= =?UTF-8?q?=E3=83=A1=E3=82=BD=E3=83=83=E3=83=89=E3=81=AE=E6=96=B9=E3=81=AB?= =?UTF-8?q?`@MainActor`=E3=82=92=E6=8C=87=E5=AE=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- macSKK/Dict.swift | 10 +++++----- macSKK/FileDict.swift | 12 ++++-------- macSKK/UserDict.swift | 2 +- macSKKTests/FileDictTests.swift | 12 ++++++------ 4 files changed, 16 insertions(+), 20 deletions(-) diff --git a/macSKK/Dict.swift b/macSKK/Dict.swift index b19464d6..71a30d82 100644 --- a/macSKK/Dict.swift +++ b/macSKK/Dict.swift @@ -29,7 +29,7 @@ struct DictLoadEvent { let status: DictLoadStatus } -@MainActor protocol DictProtocol { +protocol DictProtocol { /** * 辞書を引き変換候補順に返す * @@ -39,7 +39,7 @@ struct DictLoadEvent { * - yomi: SKK辞書の見出し。複数のひらがな、もしくは複数のひらがな + ローマ字からなる文字列 * - option: 辞書を引くときに接頭辞、接尾辞や送り仮名ブロックから検索するかどうか。nilなら通常のエントリから検索する */ - func refer(_ yomi: String, option: DictReferringOption?) -> [Word] + @MainActor func refer(_ yomi: String, option: DictReferringOption?) -> [Word] /** * 辞書にエントリを追加する。 @@ -48,7 +48,7 @@ struct DictLoadEvent { * - yomi: SKK辞書の見出し。複数のひらがな、もしくは複数のひらがな + ローマ字からなる文字列 * - word: SKK辞書の変換候補。 */ - mutating func add(yomi: String, word: Word) + @MainActor mutating func add(yomi: String, word: Word) /** * 辞書からエントリを削除する。 @@ -60,7 +60,7 @@ struct DictLoadEvent { * - word: SKK辞書の変換候補。 * - Returns: エントリを削除できたかどうか */ - mutating func delete(yomi: String, word: Word.Word) -> Bool + @MainActor mutating func delete(yomi: String, word: Word.Word) -> Bool /** * 現在入力中のprefixに続く入力候補を1つ返す。見つからなければnilを返す。 @@ -73,5 +73,5 @@ struct DictLoadEvent { * - prefixと読みが完全に一致する場合は補完候補とはしない * - 数値変換用の読みは補完候補としない */ - func findCompletion(prefix: String) -> String? + @MainActor func findCompletion(prefix: String) -> String? } diff --git a/macSKK/FileDict.swift b/macSKK/FileDict.swift index 321e0b64..dc6aeb40 100644 --- a/macSKK/FileDict.swift +++ b/macSKK/FileDict.swift @@ -238,11 +238,11 @@ enum FileDictType: Equatable { var failedEntryCount: Int { return dict.failedEntryCount } // MARK: DictProtocol - func refer(_ yomi: String, option: DictReferringOption?) -> [Word] { + @MainActor func refer(_ yomi: String, option: DictReferringOption?) -> [Word] { return dict.refer(yomi, option: option) } - func add(yomi: String, word: Word) { + @MainActor func add(yomi: String, word: Word) { dict.add(yomi: yomi, word: word) NotificationCenter.default.post(name: notificationNameDictLoad, object: DictLoadEvent(id: self.id, @@ -250,7 +250,7 @@ enum FileDictType: Equatable { hasUnsavedChanges = true } - func delete(yomi: String, word: Word.Word) -> Bool { + @MainActor func delete(yomi: String, word: Word.Word) -> Bool { if dict.delete(yomi: yomi, word: word) { hasUnsavedChanges = true NotificationCenter.default.post(name: notificationNameDictLoad, @@ -261,7 +261,7 @@ enum FileDictType: Equatable { return false } - func findCompletion(prefix: String) -> String? { + @MainActor func findCompletion(prefix: String) -> String? { return dict.findCompletion(prefix: prefix) } @@ -277,10 +277,6 @@ extension FileDict: NSFilePresenter { // どちらも同じ辞書ファイルを監視しているので、Aが保存してもAのpresentedItemDidChangeは呼び出されないが、 // BのpresentedItemDidChangeは呼び出される。 nonisolated func presentedItemDidChange() { - guard let version = NSFileVersion.currentVersionOfItem(at: fileURL) else { - logger.error("辞書 \(self.id, privacy: .public) のバージョンが存在しません") - return - } logger.log("辞書 \(self.id, privacy: .public) が変更されたので読み込みます") load(fileURL: self.fileURL) } diff --git a/macSKK/UserDict.swift b/macSKK/UserDict.swift index 89f4fa6e..6c769ad3 100644 --- a/macSKK/UserDict.swift +++ b/macSKK/UserDict.swift @@ -105,7 +105,7 @@ import Foundation * - yomi: SKK辞書の見出し。複数のひらがな、もしくは複数のひらがな + ローマ字からなる文字列 * - option: 辞書を引くときに接頭辞や接尾辞から検索するかどうか。nilなら通常のエントリから検索する */ - @MainActor func referDicts(_ yomi: String, option: DictReferringOption? = nil) -> [Candidate] { + func referDicts(_ yomi: String, option: DictReferringOption? = nil) -> [Candidate] { var result: [Candidate] = [] var candidates = refer(yomi, option: option).map { word in let annotations: [Annotation] = if let annotation = word.annotation { [annotation] } else { [] } diff --git a/macSKKTests/FileDictTests.swift b/macSKKTests/FileDictTests.swift index 978143e2..4b84601c 100644 --- a/macSKKTests/FileDictTests.swift +++ b/macSKKTests/FileDictTests.swift @@ -10,13 +10,13 @@ final class FileDictTests: XCTestCase { let fileURL = Bundle(for: FileDictTests.self).url(forResource: "empty", withExtension: "txt")! var cancellables: Set = [] - func testLoadContainsBom() throws { + @MainActor func testLoadContainsBom() throws { let fileURL = Bundle(for: Self.self).url(forResource: "utf8-bom", withExtension: "txt")! let dict = try FileDict(contentsOf: fileURL, type: .traditional(.utf8), readonly: true) XCTAssertEqual(dict.dict.entries, ["ゆにこーど": [Word("ユニコード")]]) } - func testLoadJson() throws { + @MainActor func testLoadJson() throws { let expectation = XCTestExpectation() NotificationCenter.default.publisher(for: notificationNameDictLoad).sink { notification in if let loadEvent = notification.object as? DictLoadEvent { @@ -34,7 +34,7 @@ final class FileDictTests: XCTestCase { wait(for: [expectation], timeout: 1.0) } - func testLoadJsonBroken() throws { + @MainActor func testLoadJsonBroken() throws { let expectation = XCTestExpectation() NotificationCenter.default.publisher(for: notificationNameDictLoad).sink { notification in if let loadEvent = notification.object as? DictLoadEvent { @@ -48,7 +48,7 @@ final class FileDictTests: XCTestCase { wait(for: [expectation], timeout: 1.0) } - func testAdd() throws { + @MainActor func testAdd() throws { let dict = try FileDict(contentsOf: fileURL, type: .traditional(.utf8), readonly: true) XCTAssertEqual(dict.entryCount, 0) let word = Word("井") @@ -58,7 +58,7 @@ final class FileDictTests: XCTestCase { XCTAssertTrue(dict.hasUnsavedChanges) } - func testDelete() throws { + @MainActor func testDelete() throws { let dict = try FileDict(contentsOf: fileURL, type: .traditional(.utf8), readonly: true) dict.setEntries(["あr": [Word("有"), Word("在")]], readonly: true) XCTAssertFalse(dict.delete(yomi: "あr", word: "或")) @@ -67,7 +67,7 @@ final class FileDictTests: XCTestCase { XCTAssertTrue(dict.hasUnsavedChanges) } - func testSerialize() throws { + @MainActor func testSerialize() throws { let dict = try FileDict(contentsOf: fileURL, type: .traditional(.utf8), readonly: false) XCTAssertEqual(dict.serialize(), [FileDict.headers[0], FileDict.okuriAriHeader, FileDict.okuriNashiHeader, ""].joined(separator: "\n")) From d2825ce08fcf33823c31d955608c824e8d4a452c Mon Sep 17 00:00:00 2001 From: mtgto Date: Sun, 29 Dec 2024 16:22:30 +0900 Subject: [PATCH 4/7] =?UTF-8?q?SKK=E8=BE=9E=E6=9B=B8=E3=83=95=E3=82=A1?= =?UTF-8?q?=E3=82=A4=E3=83=AB=E3=81=AE=E8=AA=AD=E3=81=BF=E8=BE=BC=E3=81=BF?= =?UTF-8?q?=E3=82=92async=E3=81=AB=E5=A4=89=E6=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- macSKK/FileDict.swift | 113 +++++++++++++----------- macSKK/Settings/SettingsViewModel.swift | 51 ++++++----- 2 files changed, 87 insertions(+), 77 deletions(-) diff --git a/macSKK/FileDict.swift b/macSKK/FileDict.swift index dc6aeb40..872116d1 100644 --- a/macSKK/FileDict.swift +++ b/macSKK/FileDict.swift @@ -57,6 +57,7 @@ enum FileDictType: Equatable { enum FileDictError: Error { case decode + case unknown } /// JSON形式 @@ -77,7 +78,7 @@ enum FileDictType: Equatable { return queue }() - init(contentsOf fileURL: URL, type: FileDictType, readonly: Bool) throws { + init(contentsOf fileURL: URL, type: FileDictType, readonly: Bool) { // iCloud Documents使うときには辞書フォルダが複数になりうるけど、それまではひとまずファイル名をIDとして使う self.id = fileURL.lastPathComponent self.fileURL = fileURL @@ -85,67 +86,71 @@ enum FileDictType: Equatable { self.dict = MemoryDict(entries: [:], readonly: readonly) self.readonly = readonly super.init() - load(fileURL: fileURL) NSFileCoordinator.addFilePresenter(self) } - nonisolated func load(fileURL: URL) { - let operation = BlockOperation { - var coordinationError: NSError? - var readingError: NSError? - let fileCoordinator = NSFileCoordinator(filePresenter: self) - NotificationCenter.default.post(name: notificationNameDictLoad, - object: DictLoadEvent(id: self.id, - status: .loading)) + // このメソッド自体はasyncにしていますがマルチスレッドを使用はしていません。 + // 呼び出し側でマルチスレッドから呼び出してください。 + // TODO: NSFileCoordinatorの非同期版を使うことを検討する + nonisolated func load(fileURL: URL) async throws { + var coordinationError: NSError? + let fileCoordinator = NSFileCoordinator(filePresenter: self) + NotificationCenter.default.post(name: notificationNameDictLoad, + object: DictLoadEvent(id: self.id, + status: .loading)) + let memoryDict: MemoryDict = try await withCheckedThrowingContinuation { continuation in fileCoordinator.coordinate(readingItemAt: fileURL, error: &coordinationError) { [weak self] url in - if let self { - do { - let memoryDict: MemoryDict - if case .json = self.type { - let decoder = JSONDecoder() - decoder.keyDecodingStrategy = .convertFromSnakeCase - let jisyo = try decoder.decode(JsonJisyo.self, from: try Data(contentsOf: url)) - if jisyo.version != "0.0.0" { - logger.warning("JSON辞書のバージョンが未対応のバージョンのため無視します") - throw FileDictError.decode - } - let okuriAriEntries = jisyo.okuriAri.mapValues({ - $0.map { Word($0) } - }) - let okuriNashiEntries = jisyo.okuriNasi.mapValues({ - $0.map { Word($0) } - }) - memoryDict = MemoryDict(okuriAriEntries: okuriAriEntries, okuriNashiEntries: okuriNashiEntries, readonly: readonly) - } else if case .traditional(let encoding) = self.type { - let source = try self.loadString(url, encoding: encoding) - if source.isEmpty { - // 辞書ファイルを書き込み中に読み込んでしまった? - logger.warning("辞書 \(self.id) を読み込んだところ0バイトだったため更新を無視します") - } - memoryDict = MemoryDict(dictId: self.id, source: source, readonly: readonly) - } else { - fatalError() + guard let self else { + continuation.resume(throwing: FileDictError.unknown) + return + } + do { + let memoryDict: MemoryDict + if case .json = self.type { + let decoder = JSONDecoder() + decoder.keyDecodingStrategy = .convertFromSnakeCase + let jisyo = try decoder.decode(JsonJisyo.self, from: try Data(contentsOf: url)) + if jisyo.version != "0.0.0" { + logger.warning("JSON辞書のバージョンが未対応のバージョンのため無視します") + throw FileDictError.decode } - Task { @MainActor in - self.dict = memoryDict + let okuriAriEntries = jisyo.okuriAri.mapValues({ + $0.map { Word($0) } + }) + let okuriNashiEntries = jisyo.okuriNasi.mapValues({ + $0.map { Word($0) } + }) + memoryDict = MemoryDict(okuriAriEntries: okuriAriEntries, okuriNashiEntries: okuriNashiEntries, readonly: readonly) + } else if case .traditional(let encoding) = self.type { + let source = try self.loadString(url, encoding: encoding) + if source.isEmpty { + // 辞書ファイルを書き込み中に読み込んでしまった? + logger.warning("辞書 \(self.id) を読み込んだところ0バイトだったため更新を無視します") } - NotificationCenter.default.post(name: notificationNameDictLoad, - object: DictLoadEvent(id: self.id, - status: .loaded(success: memoryDict.entryCount, failure: memoryDict.failedEntryCount))) - } catch { - NotificationCenter.default.post(name: notificationNameDictLoad, - object: DictLoadEvent(id: self.id, - status: .fail(error))) - readingError = error as NSError + memoryDict = MemoryDict(dictId: self.id, source: source, readonly: readonly) + } else { + fatalError() } + continuation.resume(returning: memoryDict) + NotificationCenter.default.post(name: notificationNameDictLoad, + object: DictLoadEvent(id: self.id, + status: .loaded(success: memoryDict.entryCount, + failure: memoryDict.failedEntryCount))) + } catch { + NotificationCenter.default.post(name: notificationNameDictLoad, + object: DictLoadEvent(id: self.id, + status: .fail(error))) + continuation.resume(throwing: error) } } - if let error = coordinationError ?? readingError { - logger.error("辞書 \(self.id, privacy: .public) の読み込み中にエラーが発生しました: \(error)") - } } - fileOperationQueue.addOperation(operation) - operation.waitUntilFinished() + if let error = coordinationError { + logger.error("辞書 \(self.id, privacy: .public) の読み込み中にエラーが発生しました: \(error)") + throw error + } + await MainActor.run { [memoryDict] in + self.dict = memoryDict + } } nonisolated private func loadString(_ url: URL, encoding: String.Encoding) throws -> String { @@ -278,6 +283,8 @@ extension FileDict: NSFilePresenter { // BのpresentedItemDidChangeは呼び出される。 nonisolated func presentedItemDidChange() { logger.log("辞書 \(self.id, privacy: .public) が変更されたので読み込みます") - load(fileURL: self.fileURL) + Task { + try await load(fileURL: self.fileURL) + } } } diff --git a/macSKK/Settings/SettingsViewModel.swift b/macSKK/Settings/SettingsViewModel.swift index 36c6ea3e..5766e52c 100644 --- a/macSKK/Settings/SettingsViewModel.swift +++ b/macSKK/Settings/SettingsViewModel.swift @@ -298,34 +298,37 @@ final class SettingsViewModel: ObservableObject { Global.ignoreUserDictInPrivateMode.send(ignoreUserDictInPrivateMode) // SKK-JISYO.Lのようなファイルの読み込みが遅いのでバックグラウンドで処理 - $dictSettings.filter({ !$0.isEmpty }).receive(on: DispatchQueue.global()).sink { dictSettings in - let enabledDicts = dictSettings.compactMap { dictSetting -> FileDict? in - let dict = Global.dictionary.fileDict(id: dictSetting.id) - if dictSetting.enabled { - // 無効だった辞書が有効化された、もしくは辞書のエンコーディング設定が変わったら読み込む - if dictSetting.type.encoding != dict?.type.encoding { - let fileURL = dictionariesDirectoryUrl.appendingPathComponent(dictSetting.filename) - do { - logger.log("SKK辞書 \(dictSetting.filename, privacy: .public) を読み込みます") - let fileDict = try FileDict(contentsOf: fileURL, type: dictSetting.type, readonly: true) - logger.log("SKK辞書 \(dictSetting.filename, privacy: .public) から \(fileDict.entryCount) エントリ読み込みました") - return fileDict - } catch { - dictSetting.enabled = false - logger.log("SKK辞書 \(dictSetting.filename, privacy: .public) の読み込みに失敗しました!: \(error)") - return nil + $dictSettings.filter({ !$0.isEmpty }).receive(on: DispatchQueue.global()).flatMap { dictSettings in + Deferred { + var fileDicts: [FileDict] = [] + return Future<[FileDict], Never>() { promise in + Task { + for dictSetting in dictSettings { + let dict = Global.dictionary.fileDict(id: dictSetting.id) + // 無効だった辞書が有効化された、もしくは辞書のエンコーディング設定が変わったら読み込む + if dictSetting.type.encoding != dict?.type.encoding { + let fileURL = dictionariesDirectoryUrl.appendingPathComponent(dictSetting.filename) + do { + logger.log("SKK辞書 \(dictSetting.filename, privacy: .public) を読み込みます") + let fileDict = FileDict(contentsOf: fileURL, type: dictSetting.type, readonly: true) + try await fileDict.load(fileURL: fileURL) + logger.log("SKK辞書 \(dictSetting.filename, privacy: .public) から \(fileDict.entryCount) エントリ読み込みました") + fileDicts.append(fileDict) + } catch { + dictSetting.enabled = false + logger.log("SKK辞書 \(dictSetting.filename, privacy: .public) の読み込みに失敗しました!: \(error)") + } + } else if let dict { + // 変更がないのでそのまま + fileDicts.append(dict) + } } - } else { - return dict - } - } else { - if dict != nil { - logger.log("SKK辞書 \(dictSetting.filename, privacy: .public) を無効化します") + promise(.success(fileDicts)) } - return nil } } - Global.dictionary.dicts = enabledDicts + }.sink { fileDicts in + Global.dictionary.dicts = fileDicts UserDefaults.standard.set(self.dictSettings.map { $0.encode() }, forKey: UserDefaultsKeys.dictionaries) } .store(in: &cancellables) From 6c576c6cebf3c9c073a62d2473095fdd8dfd6183 Mon Sep 17 00:00:00 2001 From: mtgto Date: Sun, 29 Dec 2024 16:52:26 +0900 Subject: [PATCH 5/7] =?UTF-8?q?=E3=83=A6=E3=83=8B=E3=83=83=E3=83=88?= =?UTF-8?q?=E3=83=86=E3=82=B9=E3=83=88=E3=82=82FileDict#load=E3=81=AB?= =?UTF-8?q?=E5=AF=BE=E5=BF=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- macSKKTests/FileDictTests.swift | 37 +++++++++++++++++++------------ macSKKTests/MemoryDictTests.swift | 10 ++++----- macSKKTests/UserDictTests.swift | 10 ++++----- 3 files changed, 33 insertions(+), 24 deletions(-) diff --git a/macSKKTests/FileDictTests.swift b/macSKKTests/FileDictTests.swift index 4b84601c..b9a0726d 100644 --- a/macSKKTests/FileDictTests.swift +++ b/macSKKTests/FileDictTests.swift @@ -10,13 +10,14 @@ final class FileDictTests: XCTestCase { let fileURL = Bundle(for: FileDictTests.self).url(forResource: "empty", withExtension: "txt")! var cancellables: Set = [] - @MainActor func testLoadContainsBom() throws { + @MainActor func testLoadContainsBom() async throws { let fileURL = Bundle(for: Self.self).url(forResource: "utf8-bom", withExtension: "txt")! - let dict = try FileDict(contentsOf: fileURL, type: .traditional(.utf8), readonly: true) + let dict = FileDict(contentsOf: fileURL, type: .traditional(.utf8), readonly: true) + try await dict.load(fileURL: fileURL) XCTAssertEqual(dict.dict.entries, ["ゆにこーど": [Word("ユニコード")]]) } - @MainActor func testLoadJson() throws { + @MainActor func testLoadJson() async throws { let expectation = XCTestExpectation() NotificationCenter.default.publisher(for: notificationNameDictLoad).sink { notification in if let loadEvent = notification.object as? DictLoadEvent { @@ -28,13 +29,13 @@ final class FileDictTests: XCTestCase { } }.store(in: &cancellables) let fileURL = Bundle(for: Self.self).url(forResource: "SKK-JISYO.test", withExtension: "json")! - let dict = try FileDict(contentsOf: fileURL, type: .json, readonly: true) + let dict = FileDict(contentsOf: fileURL, type: .json, readonly: true) + try await dict.load(fileURL: fileURL) XCTAssertEqual(dict.dict.refer("い", option: nil).map({ $0.word }).sorted(), ["伊", "胃"]) XCTAssertEqual(dict.dict.refer("あr", option: nil).map({ $0.word }).sorted(), ["在;注釈として解釈されない", "有"]) - wait(for: [expectation], timeout: 1.0) } - @MainActor func testLoadJsonBroken() throws { + @MainActor func testLoadJsonBroken() async throws { let expectation = XCTestExpectation() NotificationCenter.default.publisher(for: notificationNameDictLoad).sink { notification in if let loadEvent = notification.object as? DictLoadEvent { @@ -44,12 +45,18 @@ final class FileDictTests: XCTestCase { } }.store(in: &cancellables) let fileURL = Bundle(for: Self.self).url(forResource: "SKK-JISYO.broken", withExtension: "json")! - _ = try FileDict(contentsOf: fileURL, type: .json, readonly: true) - wait(for: [expectation], timeout: 1.0) + let dict = FileDict(contentsOf: fileURL, type: .json, readonly: true) + do { + try await dict.load(fileURL: fileURL) + XCTFail("エラーが発生するはずなのに発生していない") + } catch { + // OK + } } - @MainActor func testAdd() throws { - let dict = try FileDict(contentsOf: fileURL, type: .traditional(.utf8), readonly: true) + @MainActor func testAdd() async throws { + let dict = FileDict(contentsOf: fileURL, type: .traditional(.utf8), readonly: true) + try await dict.load(fileURL: fileURL) XCTAssertEqual(dict.entryCount, 0) let word = Word("井") XCTAssertFalse(dict.hasUnsavedChanges) @@ -58,8 +65,9 @@ final class FileDictTests: XCTestCase { XCTAssertTrue(dict.hasUnsavedChanges) } - @MainActor func testDelete() throws { - let dict = try FileDict(contentsOf: fileURL, type: .traditional(.utf8), readonly: true) + @MainActor func testDelete() async throws { + let dict = FileDict(contentsOf: fileURL, type: .traditional(.utf8), readonly: true) + try await dict.load(fileURL: fileURL) dict.setEntries(["あr": [Word("有"), Word("在")]], readonly: true) XCTAssertFalse(dict.delete(yomi: "あr", word: "或")) XCTAssertFalse(dict.hasUnsavedChanges) @@ -67,8 +75,9 @@ final class FileDictTests: XCTestCase { XCTAssertTrue(dict.hasUnsavedChanges) } - @MainActor func testSerialize() throws { - let dict = try FileDict(contentsOf: fileURL, type: .traditional(.utf8), readonly: false) + @MainActor func testSerialize() async throws { + let dict = FileDict(contentsOf: fileURL, type: .traditional(.utf8), readonly: false) + try await dict.load(fileURL: fileURL) XCTAssertEqual(dict.serialize(), [FileDict.headers[0], FileDict.okuriAriHeader, FileDict.okuriNashiHeader, ""].joined(separator: "\n")) dict.add(yomi: "あ", word: Word("亜", annotation: Annotation(dictId: "testDict", text: "亜の注釈"))) diff --git a/macSKKTests/MemoryDictTests.swift b/macSKKTests/MemoryDictTests.swift index 0c394d3c..a5f50373 100644 --- a/macSKKTests/MemoryDictTests.swift +++ b/macSKKTests/MemoryDictTests.swift @@ -89,7 +89,7 @@ class MemoryDictTests: XCTestCase { XCTAssertNil(dict.entries["から"]) } - func testAdd() throws { + @MainActor func testAdd() throws { var dict = MemoryDict(entries: [:], readonly: false) XCTAssertEqual(dict.entryCount, 0) let word1 = Word("井") @@ -120,7 +120,7 @@ class MemoryDictTests: XCTestCase { XCTAssertEqual(dict.refer("いt", option: nil), [Word("行", okuri: "った"), Word("行")]) } - func testDelete() throws { + @MainActor func testDelete() throws { var dict = MemoryDict(entries: ["あr": [Word("有"), Word("在")], "え": [Word("絵"), Word("柄")]], readonly: false) XCTAssertFalse(dict.entries.isEmpty) XCTAssertEqual(dict.okuriAriYomis, ["あr"]) @@ -145,14 +145,14 @@ class MemoryDictTests: XCTestCase { XCTAssertTrue(dict.entries.isEmpty) } - func testDeleteOkuriBlock() throws { + @MainActor func testDeleteOkuriBlock() throws { var dict = MemoryDict(entries: ["あr": [Word("有", okuri: "る"), Word("有", okuri: "り"), Word("有")]], readonly: false) XCTAssertTrue(dict.delete(yomi: "あr", word: "有")) XCTAssertEqual(dict.refer("あr", option: nil), [], "あr を読みとして持つ変換候補が全て削除された") XCTAssertEqual(dict.okuriAriYomis, []) } - func testFindCompletion() throws { + @MainActor func testFindCompletion() throws { var dict = MemoryDict(entries: [:], readonly: false) XCTAssertNil(dict.findCompletion(prefix: ""), "辞書が空だとnil") dict.add(yomi: "あいうえおか", word: Word("アイウエオカ")) @@ -166,7 +166,7 @@ class MemoryDictTests: XCTestCase { XCTAssertNil(dict.findCompletion(prefix: "だい"), "数値変換の読みはnil") } - func testReferWithOption() { + @MainActor func testReferWithOption() { let dict = MemoryDict(entries: ["あき>": [Word("空き")], "あき": [Word("秋")], ">し": [Word("氏")], diff --git a/macSKKTests/UserDictTests.swift b/macSKKTests/UserDictTests.swift index f42a6155..f13969f4 100644 --- a/macSKKTests/UserDictTests.swift +++ b/macSKKTests/UserDictTests.swift @@ -7,7 +7,7 @@ import Combine @testable import macSKK final class UserDictTests: XCTestCase { - func testRefer() throws { + @MainActor func testRefer() throws { let dict1 = MemoryDict(entries: ["い": [Word("胃"), Word("伊")]], readonly: true) let dict2 = MemoryDict(entries: ["い": [Word("胃"), Word("意")]], readonly: true) let userDict = try UserDict(dicts: [dict1, dict2], @@ -32,7 +32,7 @@ final class UserDictTests: XCTestCase { XCTAssertEqual(userDict.referDicts("い").map({ $0.annotations.map({ $0.dictId }) }), [["dict1", "dict2"], [], []]) } - func testReferWithOption() throws { + @MainActor func testReferWithOption() throws { let dict = MemoryDict(entries: ["あき>": [Word("空き")], "あき": [Word("秋")], ">し": [Word("氏")], @@ -54,7 +54,7 @@ final class UserDictTests: XCTestCase { XCTAssertEqual(userDict.refer("し", option: .prefix), []) } - func testPrivateMode() throws { + @MainActor func testPrivateMode() throws { let privateMode = CurrentValueSubject(false) let userDict = try UserDict(dicts: [], userDictEntries: ["い": [Word("位")]], @@ -72,7 +72,7 @@ final class UserDictTests: XCTestCase { XCTAssertTrue(userDict.delete(yomi: "い", word: "井")) } - func testFindCompletionPrivateMode() throws { + @MainActor func testFindCompletionPrivateMode() throws { let privateMode = CurrentValueSubject(true) let ignoreUserDictInPrivateMode = CurrentValueSubject(false) let dict1 = MemoryDict(entries: ["にほん": [Word("日本")], "にほ": [Word("2歩")]], readonly: false) @@ -92,7 +92,7 @@ final class UserDictTests: XCTestCase { XCTAssertEqual(userDict.findCompletion(prefix: "に"), "にふ") } - func testFindCompletionFromAllDicts() throws { + @MainActor func testFindCompletionFromAllDicts() throws { let privateMode = CurrentValueSubject(false) let ignoreUserDictInPrivateMode = CurrentValueSubject(false) let findCompletionFromAllDicts = CurrentValueSubject(false) From e2a4c481e1147c20cb0607eeec337e09141a8ec2 Mon Sep 17 00:00:00 2001 From: mtgto Date: Sun, 29 Dec 2024 18:26:14 +0900 Subject: [PATCH 6/7] =?UTF-8?q?UserDict#load=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- macSKK/UserDict.swift | 14 ++++++++------ macSKK/macSKKApp.swift | 3 +++ 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/macSKK/UserDict.swift b/macSKK/UserDict.swift index 6c769ad3..2a4470a7 100644 --- a/macSKK/UserDict.swift +++ b/macSKK/UserDict.swift @@ -54,12 +54,8 @@ import Foundation logger.log("ユーザー辞書ファイルがないため作成します") try Data().write(to: userDictFileURL, options: .withoutOverwriting) } - do { - let userDict = try FileDict(contentsOf: userDictFileURL, type: .traditional(.utf8), readonly: false) - self.userDict = userDict - } catch { - self.userDict = nil - } + let userDict = FileDict(contentsOf: userDictFileURL, type: .traditional(.utf8), readonly: false) + self.userDict = userDict } super.init() NSFileCoordinator.addFilePresenter(self) @@ -89,6 +85,12 @@ import Foundation NSFileCoordinator.removeFilePresenter(self) } + func load() async throws { + if let userDict = userDict as? FileDict { + try await userDict.load(fileURL: userDictFileURL) + } + } + /** * 保持する辞書を順に引き変換候補順に返す。 * diff --git a/macSKK/macSKKApp.swift b/macSKK/macSKKApp.swift index 1d33eee2..ac8de5f5 100644 --- a/macSKK/macSKKApp.swift +++ b/macSKK/macSKKApp.swift @@ -104,6 +104,9 @@ struct macSKKApp: App { setupDirectMode() setupSettingsNotification() } + Task { + try await Global.dictionary.load() + } } var body: some Scene { From cbf293e7645f8a77b549b6d50ca66219565fe9c1 Mon Sep 17 00:00:00 2001 From: mtgto Date: Sun, 29 Dec 2024 18:40:35 +0900 Subject: [PATCH 7/7] =?UTF-8?q?=E7=84=A1=E5=8A=B9=E3=81=AA=E8=BE=9E?= =?UTF-8?q?=E6=9B=B8=E3=82=92=E8=AA=AD=E3=81=BF=E8=BE=BC=E3=82=93=E3=81=A7?= =?UTF-8?q?=E3=81=84=E3=82=8B=E3=81=AE=E3=82=92=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- macSKK/Settings/SettingsViewModel.swift | 3 +++ 1 file changed, 3 insertions(+) diff --git a/macSKK/Settings/SettingsViewModel.swift b/macSKK/Settings/SettingsViewModel.swift index 5766e52c..c3e0c7a2 100644 --- a/macSKK/Settings/SettingsViewModel.swift +++ b/macSKK/Settings/SettingsViewModel.swift @@ -304,6 +304,9 @@ final class SettingsViewModel: ObservableObject { return Future<[FileDict], Never>() { promise in Task { for dictSetting in dictSettings { + if !dictSetting.enabled { + continue + } let dict = Global.dictionary.fileDict(id: dictSetting.id) // 無効だった辞書が有効化された、もしくは辞書のエンコーディング設定が変わったら読み込む if dictSetting.type.encoding != dict?.type.encoding {