📝️

SwiftUI: 読みがなを自動入力する

2024/12/22に公開

はじめに

iOS/macOSにはCFStringTokenizerというものがあって、漢字をローマ字に変換できたりします。
ですが、実際にこれを用いて漢字を読みがなにしようとすると、かなり手こずります。

自作の買い物メモアプリ「マジ買う!」の開発時に実装したのですが、なんとなく動くところまでは良かったものの、ちゃんと動くまでは結構大変でした。

本記事では、そこの処理だけを取り出したサンプルアプリを紹介します。
GitHubにあげてあります

以前の記事「SwiftUI: iOSで入力補助メニューを表示する」とあわせて見てもらえると良いかも知れません。
「マジ買う!」では、買う物マスタに表示名と読みがなの両方を持たせています。
(この読みがなも勝手に生成します。ただしiOSが読み間違えることもあるので編集できるようにしています)

買い物文字列が入力されたら、本記事の処理を用いて入力の進むつど読みがなに変換します。
そして入力した文字列での検索のほか、読みがなでも検索して候補を表示するようにしています。

動作のイメージ

サンプルアプリを動かすと、以下のように漢字を打つたびに読みがなが自動で更新されます。

打った文字を覚えておいて何かしているのではなくて、打たれている漢字を読んでいます。
「例」を打った時点では読みがなは「れい」ですが、続けて「え」を打って「例え」となった瞬間に読みがなが「たとえ」になります。

また、AIなどと違ってこの処理はローカルで動いていて外部ネットワークは使いません。

いくつか工夫した点

細かくはソースコードを見てもらえば良いのですが、いくつか工夫している点があります。

自前でローマ字をひらがなに直す

OSにローマ字→ひらがな変換をやらそうとしたんですが、どうしてもやらかすパターンが多く、それならもう自前でやるか、ということにしました。

アルファベット混在パターンをなんとかする

CFStringTokenizerはなかなか賢くて、「エアーサロンパスex」だと「えあーさろんぱすex」なんですが「エアーサロンパスEX」と大文字にすると「えあーさろんぱすいーえっくす」と読むんですね。
ですが、「いーえっくす」は良いとして、「ex」と返されるとローマ字→ひらがなにしたときに「えx」になってしまいます。

この辺をちょっと工夫してあります。

サンプルにはテストケースも付けてあります。
いくつかこのテーマに関するテストケースが入れてあります。

読めないパターンがあるのでどうにかする

CFStringTokenizerはだいたいの漢字を無理矢理読むんですが、ごく稀に読めない字があります。
トークンが進んでいるのに、ローマ字は空になります。進んだ以上、何かが必ず入っているだろうと思っていると失敗します。

使い方だけ簡単に

いちいちコードを読みたくないという人もいると思いますので、使い方だけ簡単に説明します。

冒頭のリポジトリから「YomiganaUtils.swift」というファイルだけ持ってきます。
そのファイルを自分のプロジェクトに入れます。

あとは読ませたい文字列を引数に「let yomigana = YomiganaUtils.getYomiganaOf(text)」のようにするだけです。

あら簡単。

サンプルの画面のコードも簡単なんで載せておきます。
ちょっと簡単すぎますね。

struct ContentView: View {
    @State var text: String = ""
    @State var yomigana: String = ContentView.emptyYomiganaMessage
    static let emptyYomiganaMessage = "漢字を入力するとここに読みがなが自動で入ります"
    
    var body: some View {
        Form {
            Section("漢字文字列") {
                TextField("文字列を入力", text: $text)
            }
            Section("読みがな") {
                Text(yomigana)
                    .foregroundStyle(text.isEmpty ? .tertiary : .secondary)
            }
        }
        .onChange(of: text) { (oldValue, newValue) in
            if text.isEmpty {
                yomigana = ContentView.emptyYomiganaMessage
            }
            else {
                yomigana = YomiganaUtils.getYomiganaOf(text)
            }
        }
    }
}

Discussion