SwiftUIで日本語入力時にTextFieldのクリアボタンが機能しない問題と解決策

SwiftUIで日本語入力時にTextFieldのクリアボタンが機能しない問題と解決策

Clock Icon2025.03.11

日本語入力時にTextFieldのクリアボタンが機能しない問題

Apple Developer Forumsにて「Unable to clear Japanese text in TextField」という質問が投稿されていた。

報告のあった不具合の内容は以下の通りだ。

  • ひらがなを入力中(確定前)に×ボタンを押すと → テキストが正常にクリアされる
  • ひらがなを入力して確定後に×ボタンを押すと → テキストがクリアされない
  • 日本語テキストを漢字に変換した場合 → テキストが正常にクリアされる

以下のようなコードの場合に、xボタンをタップしてもテキストが消えない現象が発生する。

import SwiftUI

struct SearchBar: View {
    @Binding private var text: String

    var body: some View {
        HStack {
            TextField("", text: $text, prompt: Text("Search"))
                .textFieldStyle(.plain)
                .padding()
                .foregroundStyle(.white)
            Button {
                text = ""
            } label: {
                Image(systemName: "xmark")
                    .foregroundStyle(.black)
            }
        }
        .padding(.horizontal, 8)
        .background(RoundedRectangle(cornerRadius: 8).fill(.gray))
        .padding(.horizontal, 8)
    }

    init(text: Binding<String>) {
        _text = text
    }
}

struct ContentView: View {
    @State var text = ""

    var body: some View {
        SearchBar(text: $text)
    }
}

#Preview {
    ContentView()
}

検証環境

  • Xcode 16.2
  • iPhone 13 Pro / iOS 18.3.1

解決案

よく見かけるUIなので回答を知りたいと考えた。UITextFieldUIViewRepresentableでラップして使う方法以外で解決できないか考えてみた。

以下に4つの解決案を紹介する。それぞれの長所短所をまとめた比較表も用意した。

解決策 長所 短所 アクセシビリティへの影響
1. フォーカスの操作 シンプルな実装 キーボードが一瞬隠れる 視覚的な変化が大きいため混乱の可能性あり
2. TextEditorの使用 確実に動作する レイアウトカスタマイズが必要 TextField と異なる動作をするため、期待と異なる体験になる可能性があるが、影響は最小限
3. UIKit設定の変更 システム標準のUIを使用 アプリ全体のTextFieldのスタイルに影響し、意図しない場所でもクリアボタンが表示される システム標準の機能を使うため、アクセシビリティの問題は少ない
4. 1文字削除後に全削除 視覚的な違和感が少ない ハックのような解決策で将来のiOSバージョンで動作しなくなる可能性がある テキスト変更の通知が2回発生するが、ユーザーには違和感なく表示される

解決案1:フォーカスを外してフォーカスを付け直す

TextFieldのフォーカス状態を制御することで問題を回避する方法だ。フォーカスを一度外してから再度フォーカスを当てることで、入力状態をリセットする。

struct SearchBar1: View {
    @Binding private var text: String
    @FocusState private var isFocused: Bool

    var body: some View {
        HStack {
            TextField("", text: $text, prompt: Text("Search"))
                .textFieldStyle(.plain)
                .padding()
                .foregroundStyle(.white)
                .focused($isFocused)
            Button {
                isFocused = false
                text = ""
                // 再フォーカスするために少し遅延させる
                DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
                    isFocused = true
                }
            } label: {
                Image(systemName: "xmark")
                    .foregroundStyle(.black)
            }
        }
        // 省略
    }

    init(text: Binding<String>) {
        _text = text
    }
}

このサンプルコードを実行してみた。フォーカスを一旦外すのでキーボードが一瞬隠れてしまう。ユーザーの使い勝手はあまり良いとは言えないだろう。

20250311220431

解決案2:TextFieldを使わずにTextEditorを使う

TextEditorTextFieldとは内部的に異なる実装となっている。

struct SearchBar2: View {
    @Binding private var text: String

    var body: some View {
        HStack {
            TextEditor(text: $text)
                .padding()
                .foregroundStyle(.white)
                .frame(height: 66)
                .scrollContentBackground(.hidden)
                .overlay(
                    Group {
                        if text.isEmpty {
                            Text("Search")
                                .foregroundStyle(.white.opacity(0.5))
                                .padding(.leading, 16)
                                .padding(.top, 24)
                                .allowsHitTesting(false)
                        }
                    },
                    alignment: .topLeading
                )
            Button {
                text = ""
            } label: {
                Image(systemName: "xmark")
                    .foregroundStyle(.black)
            }
        }
        // 省略
    }

    init(text: Binding<String>) {
        _text = text
    }
}

このサンプルコードを実行してみた。動作確認したところ、×ボタンを押すと意図通り、表示されているテキストが消えた。

20250311220449

解決案3:UITextField.appearance().clearButtonMode = .always を設定する

SwiftUIのTextFieldの背後にあるUITextFieldの設定を変更することで、システム標準のクリアボタンを表示する方法だ。

struct SearchBar3: View {
    @Binding private var text: String

    var body: some View {
        HStack {
            TextField("", text: $text, prompt: Text("Search"))
                .textFieldStyle(.plain)
                .padding()
                .foregroundStyle(.white)
        }
        // 省略
        .onAppear {
            UITextField.appearance().clearButtonMode = .always
        }
    }

    init(text: Binding<String>) {
        _text = text
    }
}

このサンプルコードを実行してみた。システム標準のクリアボタンなので、見た目も操作性も良い。

20250311220511

ただし、この方法はアプリ全体のTextFieldに影響するため採用できないケースが多いだろう。例えば、一部のTextFieldではクリアボタンを表示したくない場合や、異なるスタイルを適用したい場合に問題が生じる。必要に応じて、カスタムのUIViewRepresentableを作成して特定のTextFieldにのみ適用することを検討すべきだ。理想的には、SwiftUIのTextFieldにclearButtonMode modifierが用意されていれば良いのにと思う。

解決案4:1文字消してから次のUIループで全消しする

日本語入力の確定状態を強制的にリセットする解決案がある。まず1文字を削除してから、少し遅延させて残りのテキストを消去する方法だ。

struct SearchBar4: View {
    @Binding private var text: String

    var body: some View {
        HStack {
            TextField("", text: $text, prompt: Text("Search"))
                .textFieldStyle(.plain)
                .padding()
                .foregroundStyle(.white)
            Button {
                if !text.isEmpty {
                    let _ = text.removeLast()
                    DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
                        self.text = ""
                    }
                }
            } label: {
                Image(systemName: "xmark")
                    .foregroundStyle(.black)
            }
        }
        // 省略
    }

    init(text: Binding<String>) {
        _text = text
    }
}

この方法は、日本語入力システムの状態をリセットするためのハックとして機能する。1文字を削除することで入力システムの状態が変わり、その後の全消去が正しく機能するようになる。遅延を入れることで、UIの更新が適切に行われるようにしている。

このサンプルコードを実行してみた。操作性も悪くなく、ワークアラウンドとして良いと思う。

20250311220528

サンプルコードの全文

動作確認用のサンプルプロジェクトをGitHubの以下のリポジトリに用意した。

https://github.com/CH3COOH/SampleClearJapaneseText/

サンプルコードの全文はリポジトリを確認してほしい。

参考

解決案4は、kabeyaさんの記事で書かれている方法をベースにしている。MainActor を使うコードは Xcode 16.2 / iOS 18.3.1 シミュレータではうまく動かなかったため、代わりにDispatchQueueを使用している。

https://zenn.dev/kabeya/scraps/633161ee208e9b

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.